mirror of
https://github.com/vercel/commerce.git
synced 2025-05-17 15:06:59 +00:00
Add cart endpoints/handlers
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
parent
50f99e907a
commit
1b2904ac1f
66
packages/opencommerce/src/api/endpoints/cart/add-item.ts
Normal file
66
packages/opencommerce/src/api/endpoints/cart/add-item.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { normalizeCart } from '../../../utils/normalize'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import addCartItemsMutation from '../../mutations/add-cart-item'
|
||||
import createCartMutation from '../../mutations/create-cart'
|
||||
|
||||
import type { CartEndpoint } from '.'
|
||||
import { CreateCartPayload } from '../../../../schema'
|
||||
|
||||
const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
||||
res,
|
||||
body: { cartId, item },
|
||||
config,
|
||||
req: { cookies },
|
||||
}) => {
|
||||
if (!item) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Missing item' }],
|
||||
})
|
||||
}
|
||||
if (!item.quantity) item.quantity = 1
|
||||
|
||||
const variables = {
|
||||
input: {
|
||||
shopId: config.shopId,
|
||||
items: [
|
||||
{
|
||||
productConfiguration: {
|
||||
productId: item.productId,
|
||||
productVariantId: item.variantId,
|
||||
},
|
||||
quantity: item.quantity,
|
||||
price: item.price,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
if (!cartId) {
|
||||
const { data } = await config.fetch(createCartMutation, { variables })
|
||||
res.setHeader('Set-Cookie', [
|
||||
getCartCookie(config.cartCookie, data.cart._id, config.cartCookieMaxAge),
|
||||
getCartCookie(
|
||||
config.anonymousCartTokenCookie,
|
||||
data.token,
|
||||
config.cartCookieMaxAge
|
||||
),
|
||||
])
|
||||
|
||||
return res.status(200).json({ data: normalizeCart(data.cart) })
|
||||
}
|
||||
|
||||
const { data } = await config.fetch(addCartItemsMutation, {
|
||||
variables: {
|
||||
input: {
|
||||
items: variables.input.items,
|
||||
cartId,
|
||||
cartToken: cookies[config.anonymousCartTokenCookie],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return res.status(200).json({ data: normalizeCart(data.cart) })
|
||||
}
|
||||
|
||||
export default addItem
|
31
packages/opencommerce/src/api/endpoints/cart/get-cart.ts
Normal file
31
packages/opencommerce/src/api/endpoints/cart/get-cart.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { normalizeCart } from '../../../utils/normalize'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import getAnonymousCart from '../../queries/get-anonymous-cart'
|
||||
import type { CartEndpoint } from '.'
|
||||
|
||||
// Return current cart info
|
||||
const getCart: CartEndpoint['handlers']['getCart'] = async ({
|
||||
res,
|
||||
req: { cookies },
|
||||
body: { cartId },
|
||||
config,
|
||||
}) => {
|
||||
if (cartId && cookies[config.anonymousCartTokenCookie]) {
|
||||
const { data } = await config.fetch(getAnonymousCart, {
|
||||
variables: {
|
||||
cartId,
|
||||
cartToken: cookies[config.anonymousCartTokenCookie],
|
||||
},
|
||||
})
|
||||
|
||||
return res.status(200).json({
|
||||
data: normalizeCart(data),
|
||||
})
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
data: null,
|
||||
})
|
||||
}
|
||||
|
||||
export default getCart
|
@ -1 +1,26 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
||||
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||
import cartEndpoint from '@vercel/commerce/api/endpoints/cart'
|
||||
import type { CartSchema } from '../../../types/cart'
|
||||
import type { OpenCommerceAPI } from '../../index'
|
||||
import getCart from './get-cart'
|
||||
import addItem from './add-item'
|
||||
import updateItem from './update-item'
|
||||
import removeItem from './remove-item'
|
||||
|
||||
export type CartAPI = GetAPISchema<OpenCommerceAPI, CartSchema>
|
||||
|
||||
export type CartEndpoint = CartAPI['endpoint']
|
||||
|
||||
export const handlers: CartEndpoint['handlers'] = {
|
||||
addItem,
|
||||
getCart,
|
||||
updateItem,
|
||||
removeItem,
|
||||
}
|
||||
|
||||
const cartApi = createEndpoint<CartAPI>({
|
||||
handler: cartEndpoint,
|
||||
handlers,
|
||||
})
|
||||
|
||||
export default cartApi
|
||||
|
37
packages/opencommerce/src/api/endpoints/cart/remove-item.ts
Normal file
37
packages/opencommerce/src/api/endpoints/cart/remove-item.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { normalizeCart } from '../../../utils/normalize'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import removeCartItemsMutation from '../../mutations/remove-cart-item'
|
||||
import type { CartEndpoint } from '.'
|
||||
|
||||
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
|
||||
res,
|
||||
body: { cartId, itemId },
|
||||
config,
|
||||
req: { cookies },
|
||||
}) => {
|
||||
if (!cartId || !itemId) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Invalid request' }],
|
||||
})
|
||||
}
|
||||
|
||||
const { data } = await config.fetch(removeCartItemsMutation, {
|
||||
variables: {
|
||||
input: {
|
||||
cartId,
|
||||
cartItemIds: [itemId],
|
||||
cartToken: cookies[config.anonymousCartTokenCookie],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
res.setHeader(
|
||||
'Set-Cookie',
|
||||
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
|
||||
)
|
||||
|
||||
res.status(200).json({ data: normalizeCart(data.cart) })
|
||||
}
|
||||
|
||||
export default removeItem
|
37
packages/opencommerce/src/api/endpoints/cart/update-item.ts
Normal file
37
packages/opencommerce/src/api/endpoints/cart/update-item.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { normalizeCart } from '../../../utils/normalize'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import updateCartItemsQuantityMutation from '../../mutations/update-cart-item-quantity'
|
||||
import type { CartEndpoint } from '.'
|
||||
|
||||
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
||||
res,
|
||||
body: { cartId, itemId, item },
|
||||
config,
|
||||
req: { cookies },
|
||||
}) => {
|
||||
if (!cartId || !itemId || !item) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Invalid request' }],
|
||||
})
|
||||
}
|
||||
|
||||
const { data } = await config.fetch(updateCartItemsQuantityMutation, {
|
||||
variables: {
|
||||
input: {
|
||||
cartId,
|
||||
cartToken: cookies[config.anonymousCartTokenCookie],
|
||||
items: [{ cartItemId: itemId, quantity: item.quantity }],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Update the cart cookie
|
||||
res.setHeader(
|
||||
'Set-Cookie',
|
||||
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
|
||||
)
|
||||
res.status(200).json({ data: normalizeCart(data.cart) })
|
||||
}
|
||||
|
||||
export default updateItem
|
@ -18,6 +18,7 @@ if (!API_URL) {
|
||||
|
||||
export interface OpenCommerceConfig extends CommerceAPIConfig {
|
||||
shopId: string
|
||||
anonymousCartTokenCookie: string
|
||||
}
|
||||
|
||||
const ONE_DAY = 60 * 60 * 24
|
||||
@ -29,6 +30,7 @@ const config: OpenCommerceConfig = {
|
||||
customerCookie: 'opencommerce_customerToken',
|
||||
cartCookie: 'opencommerce_cartId',
|
||||
cartCookieMaxAge: ONE_DAY * 30,
|
||||
anonymousCartTokenCookie: 'opencommerce_anonymousCartToken',
|
||||
fetch: createFetchGraphqlApi(() => getCommerceApi().getConfig()),
|
||||
}
|
||||
|
||||
|
24
packages/opencommerce/src/api/mutations/add-cart-item.ts
Normal file
24
packages/opencommerce/src/api/mutations/add-cart-item.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import {
|
||||
cartPayloadFragment,
|
||||
incorrectPriceFailureDetailsFragment,
|
||||
minOrderQuantityFailureDetailsFragment,
|
||||
} from '../queries/get-cart-query'
|
||||
|
||||
const addCartItemsMutation = /* GraphQL */ `
|
||||
mutation addCartItemsMutation($input: AddCartItemsInput!) {
|
||||
addCartItems(input: $input) {
|
||||
cart {
|
||||
${cartPayloadFragment}
|
||||
}
|
||||
incorrectPriceFailures {
|
||||
${incorrectPriceFailureDetailsFragment}
|
||||
}
|
||||
minOrderQuantityFailures {
|
||||
${minOrderQuantityFailureDetailsFragment}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default addCartItemsMutation
|
24
packages/opencommerce/src/api/mutations/create-cart.ts
Normal file
24
packages/opencommerce/src/api/mutations/create-cart.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import {
|
||||
cartPayloadFragment,
|
||||
incorrectPriceFailureDetailsFragment,
|
||||
minOrderQuantityFailureDetailsFragment,
|
||||
} from '../queries/get-cart-query'
|
||||
|
||||
const createCartMutation = /* GraphQL */ `
|
||||
mutation createCartMutation($input: CreateCartInput!) {
|
||||
createCart(input: $input) {
|
||||
cart {
|
||||
${cartPayloadFragment}
|
||||
}
|
||||
incorrectPriceFailures {
|
||||
${incorrectPriceFailureDetailsFragment}
|
||||
}
|
||||
minOrderQuantityFailures {
|
||||
${minOrderQuantityFailureDetailsFragment}
|
||||
}
|
||||
clientMutationId
|
||||
token
|
||||
}
|
||||
}
|
||||
`
|
||||
export default createCartMutation
|
13
packages/opencommerce/src/api/mutations/remove-cart-item.ts
Normal file
13
packages/opencommerce/src/api/mutations/remove-cart-item.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { cartPayloadFragment } from '../queries/get-cart-query'
|
||||
|
||||
const removeCartItemsMutation = `
|
||||
mutation removeCartItemsMutation($input: RemoveCartItemsInput!) {
|
||||
removeCartItems(input: $input) {
|
||||
cart {
|
||||
${cartPayloadFragment}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default removeCartItemsMutation
|
@ -0,0 +1,13 @@
|
||||
import { cartPayloadFragment } from '../queries/get-cart-query'
|
||||
|
||||
const updateCartItemsQuantityMutation = `
|
||||
mutation updateCartItemsQuantity($updateCartItemsQuantityInput: UpdateCartItemsQuantityInput!) {
|
||||
updateCartItemsQuantity(input: $updateCartItemsQuantityInput) {
|
||||
cart {
|
||||
${cartPayloadFragment}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default updateCartItemsQuantityMutation
|
11
packages/opencommerce/src/api/queries/get-anonymous-cart.ts
Normal file
11
packages/opencommerce/src/api/queries/get-anonymous-cart.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { cartQueryFragment } from './get-cart-query'
|
||||
|
||||
export const getAnonymousCart = /* GraphQL */ `
|
||||
query anonymousCartByCartIdQuery($cartId: ID!, $cartToken: String!) {
|
||||
cart: anonymousCartByCartId(cartId: $cartId, cartToken: $cartToken) {
|
||||
${cartQueryFragment}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default getAnonymousCart
|
@ -0,0 +1,11 @@
|
||||
import { cartQueryFragment } from './get-cart-query'
|
||||
|
||||
const accountCartByAccountIdQuery = `
|
||||
query accountCartByAccountIdQuery($accountId: ID!, $shopId: ID!, $itemsAfterCursor: ConnectionCursor) {
|
||||
cart: accountCartByAccountId(accountId: $accountId, shopId: $shopId) {
|
||||
${cartQueryFragment}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default accountCartByAccountIdQuery
|
241
packages/opencommerce/src/api/queries/get-cart-query.ts
Normal file
241
packages/opencommerce/src/api/queries/get-cart-query.ts
Normal file
@ -0,0 +1,241 @@
|
||||
export const cartCommon = `
|
||||
_id
|
||||
createdAt
|
||||
account {
|
||||
_id
|
||||
emailRecords {
|
||||
address
|
||||
}
|
||||
}
|
||||
shop {
|
||||
_id
|
||||
currency {
|
||||
code
|
||||
}
|
||||
}
|
||||
email
|
||||
updatedAt
|
||||
expiresAt
|
||||
checkout {
|
||||
fulfillmentGroups {
|
||||
_id
|
||||
type
|
||||
data {
|
||||
shippingAddress {
|
||||
address1
|
||||
address2
|
||||
city
|
||||
company
|
||||
country
|
||||
fullName
|
||||
isBillingDefault
|
||||
isCommercial
|
||||
isShippingDefault
|
||||
phone
|
||||
postal
|
||||
region
|
||||
}
|
||||
}
|
||||
availableFulfillmentOptions {
|
||||
price {
|
||||
amount
|
||||
displayAmount
|
||||
}
|
||||
fulfillmentMethod {
|
||||
_id
|
||||
name
|
||||
displayName
|
||||
}
|
||||
}
|
||||
selectedFulfillmentOption {
|
||||
fulfillmentMethod {
|
||||
_id
|
||||
name
|
||||
displayName
|
||||
}
|
||||
price {
|
||||
amount
|
||||
displayAmount
|
||||
}
|
||||
handlingPrice {
|
||||
amount
|
||||
displayAmount
|
||||
}
|
||||
}
|
||||
shop {
|
||||
_id
|
||||
}
|
||||
shippingAddress {
|
||||
address1
|
||||
address2
|
||||
city
|
||||
company
|
||||
country
|
||||
fullName
|
||||
isBillingDefault
|
||||
isCommercial
|
||||
isShippingDefault
|
||||
phone
|
||||
postal
|
||||
region
|
||||
}
|
||||
}
|
||||
summary {
|
||||
fulfillmentTotal {
|
||||
displayAmount
|
||||
}
|
||||
itemTotal {
|
||||
amount
|
||||
displayAmount
|
||||
}
|
||||
surchargeTotal {
|
||||
amount
|
||||
displayAmount
|
||||
}
|
||||
taxTotal {
|
||||
amount
|
||||
displayAmount
|
||||
}
|
||||
total {
|
||||
amount
|
||||
currency {
|
||||
code
|
||||
}
|
||||
displayAmount
|
||||
}
|
||||
}
|
||||
}
|
||||
totalItemQuantity
|
||||
`
|
||||
|
||||
const cartItemConnectionFragment = `
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
_id
|
||||
productConfiguration {
|
||||
productId
|
||||
productVariantId
|
||||
}
|
||||
addedAt
|
||||
attributes {
|
||||
label
|
||||
value
|
||||
}
|
||||
createdAt
|
||||
isBackorder
|
||||
isLowQuantity
|
||||
isSoldOut
|
||||
imageURLs {
|
||||
large
|
||||
small
|
||||
original
|
||||
medium
|
||||
thumbnail
|
||||
}
|
||||
metafields {
|
||||
value
|
||||
key
|
||||
}
|
||||
parcel {
|
||||
length
|
||||
width
|
||||
weight
|
||||
height
|
||||
}
|
||||
price {
|
||||
amount
|
||||
displayAmount
|
||||
currency {
|
||||
code
|
||||
}
|
||||
}
|
||||
priceWhenAdded {
|
||||
amount
|
||||
displayAmount
|
||||
currency {
|
||||
code
|
||||
}
|
||||
}
|
||||
productSlug
|
||||
productType
|
||||
quantity
|
||||
shop {
|
||||
_id
|
||||
}
|
||||
subtotal {
|
||||
displayAmount
|
||||
}
|
||||
title
|
||||
productTags {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
productVendor
|
||||
variantTitle
|
||||
optionTitle
|
||||
updatedAt
|
||||
inventoryAvailableToSell
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const cartPayloadFragment = `
|
||||
${cartCommon}
|
||||
items {
|
||||
${cartItemConnectionFragment}
|
||||
}
|
||||
`
|
||||
|
||||
export const incorrectPriceFailureDetailsFragment = `
|
||||
currentPrice {
|
||||
amount
|
||||
currency {
|
||||
code
|
||||
}
|
||||
displayAmount
|
||||
}
|
||||
productConfiguration {
|
||||
productId
|
||||
productVariantId
|
||||
}
|
||||
providedPrice {
|
||||
amount
|
||||
currency {
|
||||
code
|
||||
}
|
||||
displayAmount
|
||||
}
|
||||
`
|
||||
|
||||
export const minOrderQuantityFailureDetailsFragment = `
|
||||
minOrderQuantity
|
||||
productConfiguration {
|
||||
productId
|
||||
productVariantId
|
||||
}
|
||||
quantity
|
||||
`
|
||||
|
||||
const getCartQuery = /* GraphQL */ `
|
||||
query($checkoutId: ID!) {
|
||||
node(id: $checkoutId) {
|
||||
... on Checkout {
|
||||
${cartCommon}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const cartQueryFragment = `
|
||||
${cartCommon}
|
||||
items(first: 20, after: $itemsAfterCursor) {
|
||||
${cartItemConnectionFragment}
|
||||
}
|
||||
`
|
||||
|
||||
export default getCartQuery
|
20
packages/opencommerce/src/api/utils/get-cart-cookie.ts
Normal file
20
packages/opencommerce/src/api/utils/get-cart-cookie.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { serialize, CookieSerializeOptions } from 'cookie'
|
||||
|
||||
export default function getCartCookie(
|
||||
name: string,
|
||||
cartId?: string,
|
||||
maxAge?: number
|
||||
) {
|
||||
const options: CookieSerializeOptions =
|
||||
cartId && maxAge
|
||||
? {
|
||||
maxAge,
|
||||
expires: new Date(Date.now() + maxAge * 1000),
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
}
|
||||
: { maxAge: -1, path: '/' } // Removes the cookie
|
||||
|
||||
return serialize(name, cartId || '', options)
|
||||
}
|
23
packages/opencommerce/src/types/cart.ts
Normal file
23
packages/opencommerce/src/types/cart.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import * as Core from '@vercel/commerce/types/cart'
|
||||
|
||||
export * from '@vercel/commerce/types/cart'
|
||||
|
||||
export type Cart = Core.Cart & {
|
||||
lineItems: Core.LineItem[]
|
||||
id: string
|
||||
}
|
||||
|
||||
export type CartItemBody = Core.CartItemBody & {
|
||||
price: {
|
||||
amount: number
|
||||
currency: string
|
||||
}
|
||||
}
|
||||
|
||||
export type CartTypes = {
|
||||
cart: Cart
|
||||
item: Core.LineItem
|
||||
itemBody: CartItemBody
|
||||
}
|
||||
|
||||
export type CartSchema = Core.CartSchema<CartTypes>
|
@ -10,7 +10,11 @@ import {
|
||||
CatalogProduct,
|
||||
CatalogProductVariant,
|
||||
ImageInfo,
|
||||
Cart as OCCart,
|
||||
CartItemEdge,
|
||||
CartItem,
|
||||
} from '../../schema'
|
||||
import { Cart, LineItem } from '../types/cart'
|
||||
|
||||
const normalizeProductImages = (images: ImageInfo[], name: string) =>
|
||||
images.map((image) => ({
|
||||
@ -227,3 +231,74 @@ export function normalizeVendors({ name }: OCVendor): Vendor {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeCart(cart: OCCart): Cart {
|
||||
return {
|
||||
id: cart._id,
|
||||
customerId: cart.account?._id ?? '',
|
||||
email:
|
||||
(cart.account?.emailRecords && cart.account?.emailRecords[0]?.address) ??
|
||||
'',
|
||||
|
||||
createdAt: cart.createdAt,
|
||||
currency: {
|
||||
code: cart.checkout?.summary?.total?.currency.code ?? '',
|
||||
},
|
||||
lineItems:
|
||||
cart.items?.edges?.map((cartItem) =>
|
||||
normalizeLineItem(<CartItemEdge>cartItem)
|
||||
) ?? [],
|
||||
lineItemsSubtotalPrice: +(cart.checkout?.summary?.itemTotal?.amount ?? 0),
|
||||
subtotalPrice: +(cart.checkout?.summary?.itemTotal?.amount ?? 0),
|
||||
totalPrice: cart.checkout?.summary?.total?.amount ?? 0,
|
||||
discounts: [],
|
||||
taxesIncluded: !!cart.checkout?.summary?.taxTotal?.amount,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLineItem(cartItemEdge: CartItemEdge): LineItem {
|
||||
const cartItem = cartItemEdge.node
|
||||
|
||||
if (!cartItem) {
|
||||
return <LineItem>{}
|
||||
}
|
||||
|
||||
const {
|
||||
_id,
|
||||
compareAtPrice,
|
||||
imageURLs,
|
||||
title,
|
||||
productConfiguration,
|
||||
priceWhenAdded,
|
||||
optionTitle,
|
||||
variantTitle,
|
||||
quantity,
|
||||
} = <CartItem>cartItem
|
||||
|
||||
return {
|
||||
id: _id,
|
||||
variantId: String(productConfiguration?.productVariantId),
|
||||
productId: String(productConfiguration?.productId),
|
||||
name: `${title}`,
|
||||
quantity,
|
||||
variant: {
|
||||
id: String(productConfiguration?.productVariantId),
|
||||
sku: String(productConfiguration?.productVariantId),
|
||||
name: String(optionTitle || variantTitle),
|
||||
image: {
|
||||
url: imageURLs?.thumbnail ?? '/product-img-placeholder.svg',
|
||||
},
|
||||
requiresShipping: true,
|
||||
price: priceWhenAdded?.amount,
|
||||
listPrice: compareAtPrice?.amount ?? 0,
|
||||
},
|
||||
path: '',
|
||||
discounts: [],
|
||||
options: [
|
||||
{
|
||||
value: String(optionTitle || variantTitle),
|
||||
name: String(optionTitle || variantTitle),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user