mirror of
https://github.com/vercel/commerce.git
synced 2025-07-04 12:11:22 +00:00
Implement cart
This commit is contained in:
parent
7bdefa3f48
commit
72cc34d8c7
77
framework/ordercloud/api/endpoints/cart/add-item.ts
Normal file
77
framework/ordercloud/api/endpoints/cart/add-item.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import type { CartEndpoint } from '.'
|
||||||
|
import type { RawVariant } from '../../../types/product'
|
||||||
|
import type { OrdercloudLineItem } from '../../../types/cart'
|
||||||
|
|
||||||
|
import { serialize } from 'cookie'
|
||||||
|
|
||||||
|
import { formatCart } from '../../utils/cart'
|
||||||
|
|
||||||
|
const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
||||||
|
res,
|
||||||
|
body: { cartId, item },
|
||||||
|
config: { fetch, cartCookie },
|
||||||
|
}) => {
|
||||||
|
// Return an error if no item is present
|
||||||
|
if (!item) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Missing item' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the quantity if not present
|
||||||
|
if (!item.quantity) item.quantity = 1
|
||||||
|
|
||||||
|
// Create an order if it doesn't exist
|
||||||
|
if (!cartId) {
|
||||||
|
cartId = await fetch('POST', `/orders/Outgoing`, {}).then(
|
||||||
|
(response: { ID: string }) => response.ID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the cart cookie
|
||||||
|
res.setHeader(
|
||||||
|
'Set-Cookie',
|
||||||
|
serialize(cartCookie, cartId, {
|
||||||
|
maxAge: 60 * 60 * 24 * 30,
|
||||||
|
expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'lax',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store specs
|
||||||
|
let specs: RawVariant['Specs'] = []
|
||||||
|
|
||||||
|
// If a variant is present, fetch its specs
|
||||||
|
if (item.variantId) {
|
||||||
|
specs = await fetch(
|
||||||
|
'GET',
|
||||||
|
`/me/products/${item.productId}/variants/${item.variantId}`
|
||||||
|
).then((res: RawVariant) => res.Specs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the item to the order
|
||||||
|
await fetch('POST', `/orders/Outgoing/${cartId}/lineitems`, {
|
||||||
|
ProductID: item.productId,
|
||||||
|
Quantity: item.quantity,
|
||||||
|
Specs: specs,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get cart
|
||||||
|
const [cart, lineItems] = await Promise.all([
|
||||||
|
fetch('GET', `/orders/Outgoing/${cartId}`),
|
||||||
|
fetch('GET', `/orders/Outgoing/${cartId}/lineitems`).then(
|
||||||
|
(response: { Items: OrdercloudLineItem[] }) => response.Items
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Format cart
|
||||||
|
const formattedCart = formatCart(cart, lineItems)
|
||||||
|
|
||||||
|
// Return cart and errors
|
||||||
|
res.status(200).json({ data: formattedCart, errors: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default addItem
|
51
framework/ordercloud/api/endpoints/cart/get-cart.ts
Normal file
51
framework/ordercloud/api/endpoints/cart/get-cart.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import type { OrdercloudLineItem } from '../../../types/cart'
|
||||||
|
import type { CartEndpoint } from '.'
|
||||||
|
|
||||||
|
import { serialize } from 'cookie'
|
||||||
|
|
||||||
|
import { formatCart } from '../../utils/cart'
|
||||||
|
|
||||||
|
// Return current cart info
|
||||||
|
const getCart: CartEndpoint['handlers']['getCart'] = async ({
|
||||||
|
res,
|
||||||
|
body: { cartId },
|
||||||
|
config: { fetch, cartCookie },
|
||||||
|
}) => {
|
||||||
|
if (!cartId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Invalid request' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get cart
|
||||||
|
const cart = await fetch('GET', `/orders/Outgoing/${cartId}`)
|
||||||
|
|
||||||
|
// Get line items
|
||||||
|
const lineItems = await fetch(
|
||||||
|
'GET',
|
||||||
|
`/orders/Outgoing/${cartId}/lineitems`
|
||||||
|
).then((response: { Items: OrdercloudLineItem[] }) => response.Items)
|
||||||
|
|
||||||
|
// Format cart
|
||||||
|
const formattedCart = formatCart(cart, lineItems)
|
||||||
|
|
||||||
|
// Return cart and errors
|
||||||
|
res.status(200).json({ data: formattedCart, errors: [] })
|
||||||
|
} catch (error) {
|
||||||
|
// Reset cart cookie
|
||||||
|
res.setHeader(
|
||||||
|
'Set-Cookie',
|
||||||
|
serialize(cartCookie, cartId, {
|
||||||
|
maxAge: -1,
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return empty cart
|
||||||
|
res.status(200).json({ data: null, errors: [] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getCart
|
36
framework/ordercloud/api/endpoints/cart/remove-item.ts
Normal file
36
framework/ordercloud/api/endpoints/cart/remove-item.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import type { CartEndpoint } from '.'
|
||||||
|
|
||||||
|
import { formatCart } from '../../utils/cart'
|
||||||
|
import { OrdercloudLineItem } from '../../../types/cart'
|
||||||
|
|
||||||
|
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
|
||||||
|
res,
|
||||||
|
body: { cartId, itemId },
|
||||||
|
config: { fetch },
|
||||||
|
}) => {
|
||||||
|
if (!cartId || !itemId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Invalid request' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the item to the order
|
||||||
|
await fetch('DELETE', `/orders/Outgoing/${cartId}/lineitems/${itemId}`)
|
||||||
|
|
||||||
|
// Get cart
|
||||||
|
const [cart, lineItems] = await Promise.all([
|
||||||
|
fetch('GET', `/orders/Outgoing/${cartId}`),
|
||||||
|
fetch('GET', `/orders/Outgoing/${cartId}/lineitems`).then(
|
||||||
|
(response: { Items: OrdercloudLineItem[] }) => response.Items
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Format cart
|
||||||
|
const formattedCart = formatCart(cart, lineItems)
|
||||||
|
|
||||||
|
// Return cart and errors
|
||||||
|
res.status(200).json({ data: formattedCart, errors: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default removeItem
|
52
framework/ordercloud/api/endpoints/cart/update-item.ts
Normal file
52
framework/ordercloud/api/endpoints/cart/update-item.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import type { OrdercloudLineItem } from '../../../types/cart'
|
||||||
|
import type { RawVariant } from '../../../types/product'
|
||||||
|
import type { CartEndpoint } from '.'
|
||||||
|
|
||||||
|
import { formatCart } from '../../utils/cart'
|
||||||
|
|
||||||
|
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
||||||
|
res,
|
||||||
|
body: { cartId, itemId, item },
|
||||||
|
config: { fetch },
|
||||||
|
}) => {
|
||||||
|
if (!cartId || !itemId || !item) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Invalid request' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store specs
|
||||||
|
let specs: RawVariant['Specs'] = []
|
||||||
|
|
||||||
|
// If a variant is present, fetch its specs
|
||||||
|
if (item.variantId) {
|
||||||
|
specs = await fetch(
|
||||||
|
'GET',
|
||||||
|
`/me/products/${item.productId}/variants/${item.variantId}`
|
||||||
|
).then((res: RawVariant) => res.Specs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the item to the order
|
||||||
|
await fetch('PATCH', `/orders/Outgoing/${cartId}/lineitems/${itemId}`, {
|
||||||
|
ProductID: item.productId,
|
||||||
|
Quantity: item.quantity,
|
||||||
|
Specs: specs,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get cart
|
||||||
|
const [cart, lineItems] = await Promise.all([
|
||||||
|
fetch('GET', `/orders/Outgoing/${cartId}`),
|
||||||
|
fetch('GET', `/orders/Outgoing/${cartId}/lineitems`).then(
|
||||||
|
(response: { Items: OrdercloudLineItem[] }) => response.Items
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Format cart
|
||||||
|
const formattedCart = formatCart(cart, lineItems)
|
||||||
|
|
||||||
|
// Return cart and errors
|
||||||
|
res.status(200).json({ data: formattedCart, errors: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default updateItem
|
@ -1 +0,0 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
|
41
framework/ordercloud/api/utils/cart.ts
Normal file
41
framework/ordercloud/api/utils/cart.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import type { Cart, OrdercloudCart, OrdercloudLineItem } from '../../types/cart'
|
||||||
|
|
||||||
|
export function formatCart(
|
||||||
|
cart: OrdercloudCart,
|
||||||
|
lineItems: OrdercloudLineItem[]
|
||||||
|
): Cart {
|
||||||
|
return {
|
||||||
|
id: cart.ID,
|
||||||
|
customerId: cart.FromUserID,
|
||||||
|
email: cart.FromUser.Email,
|
||||||
|
createdAt: cart.DateCreated,
|
||||||
|
currency: {
|
||||||
|
code: cart.FromUser?.xp?.currency ?? 'USD',
|
||||||
|
},
|
||||||
|
taxesIncluded: cart.TaxCost === 0,
|
||||||
|
lineItems: lineItems.map((lineItem) => ({
|
||||||
|
id: lineItem.ID,
|
||||||
|
variantId: lineItem.Variant ? String(lineItem.Variant.ID) : '',
|
||||||
|
productId: lineItem.ProductID,
|
||||||
|
name: lineItem.Product.Name,
|
||||||
|
quantity: lineItem.Quantity,
|
||||||
|
discounts: [],
|
||||||
|
path: lineItem.ProductID,
|
||||||
|
variant: {
|
||||||
|
id: lineItem.Variant ? String(lineItem.Variant.ID) : '',
|
||||||
|
sku: lineItem.ID,
|
||||||
|
name: lineItem.Product.Name,
|
||||||
|
image: {
|
||||||
|
url: lineItem.Product.xp?.Images?.[0]?.url,
|
||||||
|
},
|
||||||
|
requiresShipping: Boolean(lineItem.ShippingAddress),
|
||||||
|
price: lineItem.UnitPrice,
|
||||||
|
listPrice: lineItem.UnitPrice,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
lineItemsSubtotalPrice: cart.Subtotal,
|
||||||
|
subtotalPrice: cart.Subtotal,
|
||||||
|
totalPrice: cart.Total,
|
||||||
|
discounts: [],
|
||||||
|
}
|
||||||
|
}
|
@ -1,106 +0,0 @@
|
|||||||
import type { OrdercloudConfig } from '../index'
|
|
||||||
|
|
||||||
import { FetcherError } from '@commerce/utils/errors'
|
|
||||||
import fetch from './fetch'
|
|
||||||
|
|
||||||
const fetchRestApi: (
|
|
||||||
getConfig: () => OrdercloudConfig
|
|
||||||
) => <T>(
|
|
||||||
method: string,
|
|
||||||
resource: string,
|
|
||||||
body?: Record<string, unknown>,
|
|
||||||
fetchOptions?: Record<string, any>
|
|
||||||
) => Promise<T> =
|
|
||||||
(getConfig) =>
|
|
||||||
async <T>(
|
|
||||||
method: string,
|
|
||||||
resource: string,
|
|
||||||
body?: Record<string, unknown>,
|
|
||||||
fetchOptions?: Record<string, any>
|
|
||||||
) => {
|
|
||||||
const { commerceUrl } = getConfig()
|
|
||||||
|
|
||||||
async function getToken() {
|
|
||||||
// If not, get a new one and store it
|
|
||||||
const authResponse = await fetch(`${commerceUrl}/oauth/token`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
body: `client_id=${process.env.NEXT_PUBLIC_ORDERCLOUD_CLIENT_ID}&grant_type=client_credentials&client_secret=${process.env.NEXT_PUBLIC_ORDERCLOUD_CLIENT_SECRET}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
// If something failed getting the auth response
|
|
||||||
if (!authResponse.ok) {
|
|
||||||
// Get the body of it
|
|
||||||
const error = await authResponse.json()
|
|
||||||
|
|
||||||
// And return an error
|
|
||||||
throw new FetcherError({
|
|
||||||
errors: [{ message: error.error_description.Code }],
|
|
||||||
status: error.error_description.HttpStatus,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// If everything is fine, store the access token in global.token
|
|
||||||
global.token = await authResponse
|
|
||||||
.json()
|
|
||||||
.then((response) => response.access_token)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchData(retries = 0): Promise<T> {
|
|
||||||
// Do the request with the correct headers
|
|
||||||
const dataResponse = await fetch(`${commerceUrl}/v1${resource}`, {
|
|
||||||
...fetchOptions,
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
...fetchOptions?.headers,
|
|
||||||
accept: 'application/json, text/plain, */*',
|
|
||||||
authorization: `Bearer ${global.token}`,
|
|
||||||
},
|
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
// If something failed getting the data response
|
|
||||||
if (!dataResponse.ok) {
|
|
||||||
// If token is expired
|
|
||||||
if (dataResponse.status === 401) {
|
|
||||||
// Reset it
|
|
||||||
global.token = null
|
|
||||||
|
|
||||||
// Get a new one
|
|
||||||
await getToken()
|
|
||||||
|
|
||||||
// And if retries left
|
|
||||||
if (retries < 2) {
|
|
||||||
// Refetch
|
|
||||||
return fetchData(retries + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the body of it
|
|
||||||
const error = await dataResponse.json()
|
|
||||||
|
|
||||||
// And return an error
|
|
||||||
throw new FetcherError({
|
|
||||||
errors: [{ message: error.error_description.Code }],
|
|
||||||
status: error.error_description.HttpStatus,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return data response
|
|
||||||
return dataResponse.json() as Promise<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have a token stored
|
|
||||||
if (!global.token) {
|
|
||||||
// If not, get a new one and store it
|
|
||||||
await getToken()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the data and specify the expected type
|
|
||||||
return fetchData()
|
|
||||||
}
|
|
||||||
|
|
||||||
export default fetchRestApi
|
|
@ -1,17 +1,48 @@
|
|||||||
|
import type { AddItemHook } from '@commerce/types/cart'
|
||||||
|
import type { MutationHook } from '@commerce/utils/types'
|
||||||
|
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import { CommerceError } from '@commerce/utils/errors'
|
||||||
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
|
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
|
||||||
import { MutationHook } from '@commerce/utils/types'
|
import useCart from './use-cart'
|
||||||
|
|
||||||
export default useAddItem as UseAddItem<typeof handler>
|
export default useAddItem as UseAddItem<typeof handler>
|
||||||
export const handler: MutationHook<any> = {
|
|
||||||
|
export const handler: MutationHook<AddItemHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
query: '',
|
url: '/api/cart',
|
||||||
|
method: 'POST',
|
||||||
},
|
},
|
||||||
async fetcher({ input, options, fetch }) {},
|
async fetcher({ input: item, options, fetch }) {
|
||||||
useHook:
|
if (
|
||||||
({ fetch }) =>
|
item.quantity &&
|
||||||
() => {
|
(!Number.isInteger(item.quantity) || item.quantity! < 1)
|
||||||
return async function addItem() {
|
) {
|
||||||
return {}
|
throw new CommerceError({
|
||||||
|
message: 'The item quantity has to be a valid integer greater than 0',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = await fetch({
|
||||||
|
...options,
|
||||||
|
body: { item },
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
useHook: ({ fetch }) =>
|
||||||
|
function useHook() {
|
||||||
|
const { mutate } = useCart()
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
async function addItem(input) {
|
||||||
|
const data = await fetch({ input })
|
||||||
|
|
||||||
|
await mutate(data, false)
|
||||||
|
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
[fetch, mutate]
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,33 @@
|
|||||||
|
import type { GetCartHook } from '@commerce/types/cart'
|
||||||
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { SWRHook } from '@commerce/utils/types'
|
import { SWRHook } from '@commerce/utils/types'
|
||||||
import useCart, { UseCart } from '@commerce/cart/use-cart'
|
import useCart, { UseCart } from '@commerce/cart/use-cart'
|
||||||
|
|
||||||
export default useCart as UseCart<typeof handler>
|
export default useCart as UseCart<typeof handler>
|
||||||
|
|
||||||
export const handler: SWRHook<any> = {
|
export const handler: SWRHook<GetCartHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
query: '',
|
url: '/api/cart',
|
||||||
|
method: 'GET',
|
||||||
},
|
},
|
||||||
async fetcher() {
|
useHook: ({ useData }) =>
|
||||||
return {
|
function useHook(input) {
|
||||||
id: '',
|
const response = useData({
|
||||||
createdAt: '',
|
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
|
||||||
currency: { code: '' },
|
})
|
||||||
taxesIncluded: '',
|
|
||||||
lineItems: [],
|
|
||||||
lineItemsSubtotalPrice: '',
|
|
||||||
subtotalPrice: 0,
|
|
||||||
totalPrice: 0,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
useHook:
|
|
||||||
({ useData }) =>
|
|
||||||
(input) => {
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() =>
|
() =>
|
||||||
Object.create(
|
Object.create(response, {
|
||||||
{},
|
|
||||||
{
|
|
||||||
isEmpty: {
|
isEmpty: {
|
||||||
get() {
|
get() {
|
||||||
return true
|
return (response.data?.lineItems?.length ?? 0) <= 0
|
||||||
},
|
},
|
||||||
enumerable: true,
|
enumerable: true,
|
||||||
},
|
},
|
||||||
}
|
}),
|
||||||
),
|
[response]
|
||||||
[]
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,60 @@
|
|||||||
import { MutationHook } from '@commerce/utils/types'
|
import type {
|
||||||
|
MutationHookContext,
|
||||||
|
HookFetcherContext,
|
||||||
|
} from '@commerce/utils/types'
|
||||||
|
import type { Cart, LineItem, RemoveItemHook } from '@commerce/types/cart'
|
||||||
|
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
import { ValidationError } from '@commerce/utils/errors'
|
||||||
import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
|
import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
|
||||||
|
|
||||||
|
import useCart from './use-cart'
|
||||||
|
|
||||||
|
export type RemoveItemFn<T = any> = T extends LineItem
|
||||||
|
? (input?: RemoveItemActionInput<T>) => Promise<Cart | null | undefined>
|
||||||
|
: (input: RemoveItemActionInput<T>) => Promise<Cart | null>
|
||||||
|
|
||||||
|
export type RemoveItemActionInput<T = any> = T extends LineItem
|
||||||
|
? Partial<RemoveItemHook['actionInput']>
|
||||||
|
: RemoveItemHook['actionInput']
|
||||||
|
|
||||||
export default useRemoveItem as UseRemoveItem<typeof handler>
|
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||||
|
|
||||||
export const handler: MutationHook<any> = {
|
export const handler = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
query: '',
|
url: '/api/cart',
|
||||||
|
method: 'DELETE',
|
||||||
},
|
},
|
||||||
async fetcher({ input, options, fetch }) {},
|
async fetcher({
|
||||||
useHook:
|
input: { itemId },
|
||||||
({ fetch }) =>
|
options,
|
||||||
() => {
|
fetch,
|
||||||
return async function removeItem(input) {
|
}: HookFetcherContext<RemoveItemHook>) {
|
||||||
return {}
|
return await fetch({ ...options, body: { itemId } })
|
||||||
|
},
|
||||||
|
useHook: ({ fetch }: MutationHookContext<RemoveItemHook>) =>
|
||||||
|
function useHook<T extends LineItem | undefined = undefined>(
|
||||||
|
ctx: { item?: T } = {}
|
||||||
|
) {
|
||||||
|
const { item } = ctx
|
||||||
|
const { mutate } = useCart()
|
||||||
|
const removeItem: RemoveItemFn<LineItem> = async (input) => {
|
||||||
|
const itemId = input?.id ?? item?.id
|
||||||
|
|
||||||
|
if (!itemId) {
|
||||||
|
throw new ValidationError({
|
||||||
|
message: 'Invalid input used for this operation',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = await fetch({ input: { itemId } })
|
||||||
|
|
||||||
|
await mutate(data, false)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,93 @@
|
|||||||
|
import type {
|
||||||
|
HookFetcherContext,
|
||||||
|
MutationHookContext,
|
||||||
|
} from '@commerce/utils/types'
|
||||||
|
import type { UpdateItemHook, LineItem } from '@commerce/types/cart'
|
||||||
|
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import debounce from 'lodash.debounce'
|
||||||
|
|
||||||
import { MutationHook } from '@commerce/utils/types'
|
import { MutationHook } from '@commerce/utils/types'
|
||||||
|
import { ValidationError } from '@commerce/utils/errors'
|
||||||
import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item'
|
import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item'
|
||||||
|
|
||||||
|
import { handler as removeItemHandler } from './use-remove-item'
|
||||||
|
import useCart from './use-cart'
|
||||||
|
|
||||||
|
export type UpdateItemActionInput<T = any> = T extends LineItem
|
||||||
|
? Partial<UpdateItemHook['actionInput']>
|
||||||
|
: UpdateItemHook['actionInput']
|
||||||
|
|
||||||
export default useUpdateItem as UseUpdateItem<any>
|
export default useUpdateItem as UseUpdateItem<any>
|
||||||
|
|
||||||
export const handler: MutationHook<any> = {
|
export const handler: MutationHook<any> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
query: '',
|
url: '/api/cart',
|
||||||
|
method: 'PUT',
|
||||||
},
|
},
|
||||||
async fetcher({ input, options, fetch }) {},
|
async fetcher({
|
||||||
useHook:
|
input: { itemId, item },
|
||||||
({ fetch }) =>
|
options,
|
||||||
() => {
|
fetch,
|
||||||
return async function addItem() {
|
}: HookFetcherContext<UpdateItemHook>) {
|
||||||
return {}
|
if (Number.isInteger(item.quantity)) {
|
||||||
|
// Also allow the update hook to remove an item if the quantity is lower than 1
|
||||||
|
if (item.quantity! < 1) {
|
||||||
|
return removeItemHandler.fetcher({
|
||||||
|
options: removeItemHandler.fetchOptions,
|
||||||
|
input: { itemId },
|
||||||
|
fetch,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
} else if (item.quantity) {
|
||||||
|
throw new ValidationError({
|
||||||
|
message: 'The item quantity has to be a valid integer',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return await fetch({
|
||||||
|
...options,
|
||||||
|
body: { itemId, item },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
useHook: ({ fetch }: MutationHookContext<UpdateItemHook>) =>
|
||||||
|
function useHook<T extends LineItem | undefined = undefined>(
|
||||||
|
ctx: {
|
||||||
|
item?: T
|
||||||
|
wait?: number
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
const { item } = ctx
|
||||||
|
const { mutate } = useCart() as any
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
debounce(async (input: UpdateItemActionInput<T>) => {
|
||||||
|
const itemId = input.id ?? item?.id
|
||||||
|
const productId = input.productId ?? item?.productId
|
||||||
|
const variantId = input.productId ?? item?.variantId
|
||||||
|
|
||||||
|
if (!itemId || !productId) {
|
||||||
|
throw new ValidationError({
|
||||||
|
message: 'Invalid input used for this operation',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fetch({
|
||||||
|
input: {
|
||||||
|
itemId,
|
||||||
|
item: {
|
||||||
|
productId,
|
||||||
|
variantId: variantId || '',
|
||||||
|
quantity: input.quantity,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await mutate(data, false)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}, ctx.wait ?? 500),
|
||||||
|
[fetch, mutate]
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
126
framework/ordercloud/types/cart.ts
Normal file
126
framework/ordercloud/types/cart.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import * as Core from '@commerce/types/cart'
|
||||||
|
|
||||||
|
export * from '@commerce/types/cart'
|
||||||
|
|
||||||
|
export interface OrdercloudCart {
|
||||||
|
ID: string
|
||||||
|
FromUser: {
|
||||||
|
ID: string
|
||||||
|
Username: string
|
||||||
|
Password: null
|
||||||
|
FirstName: string
|
||||||
|
LastName: string
|
||||||
|
Email: string
|
||||||
|
Phone: null
|
||||||
|
TermsAccepted: null
|
||||||
|
Active: true
|
||||||
|
xp: {
|
||||||
|
something: string
|
||||||
|
currency: string
|
||||||
|
}
|
||||||
|
AvailableRoles: null
|
||||||
|
DateCreated: string
|
||||||
|
PasswordLastSetDate: null
|
||||||
|
}
|
||||||
|
FromCompanyID: string
|
||||||
|
ToCompanyID: string
|
||||||
|
FromUserID: string
|
||||||
|
BillingAddressID: null
|
||||||
|
BillingAddress: null
|
||||||
|
ShippingAddressID: null
|
||||||
|
Comments: null
|
||||||
|
LineItemCount: number
|
||||||
|
Status: string
|
||||||
|
DateCreated: string
|
||||||
|
DateSubmitted: null
|
||||||
|
DateApproved: null
|
||||||
|
DateDeclined: null
|
||||||
|
DateCanceled: null
|
||||||
|
DateCompleted: null
|
||||||
|
LastUpdated: string
|
||||||
|
Subtotal: number
|
||||||
|
ShippingCost: number
|
||||||
|
TaxCost: number
|
||||||
|
PromotionDiscount: number
|
||||||
|
Total: number
|
||||||
|
IsSubmitted: false
|
||||||
|
xp: {
|
||||||
|
productId: string
|
||||||
|
variantId: string
|
||||||
|
quantity: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrdercloudLineItem {
|
||||||
|
ID: string
|
||||||
|
ProductID: string
|
||||||
|
Quantity: 1
|
||||||
|
DateAdded: string
|
||||||
|
QuantityShipped: number
|
||||||
|
UnitPrice: number
|
||||||
|
PromotionDiscount: number
|
||||||
|
LineTotal: number
|
||||||
|
LineSubtotal: number
|
||||||
|
CostCenter: null
|
||||||
|
DateNeeded: null
|
||||||
|
ShippingAccount: null
|
||||||
|
ShippingAddressID: null
|
||||||
|
ShipFromAddressID: null
|
||||||
|
Product: {
|
||||||
|
ID: string
|
||||||
|
Name: string
|
||||||
|
Description: string
|
||||||
|
QuantityMultiplier: number
|
||||||
|
ShipWeight: number
|
||||||
|
ShipHeight: null
|
||||||
|
ShipWidth: null
|
||||||
|
ShipLength: null
|
||||||
|
xp: {
|
||||||
|
Images: {
|
||||||
|
url: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Variant: null | {
|
||||||
|
ID: string
|
||||||
|
Name: null
|
||||||
|
Description: null
|
||||||
|
ShipWeight: null
|
||||||
|
ShipHeight: null
|
||||||
|
ShipWidth: null
|
||||||
|
ShipLength: null
|
||||||
|
xp: null
|
||||||
|
}
|
||||||
|
ShippingAddress: null
|
||||||
|
ShipFromAddress: null
|
||||||
|
SupplierID: null
|
||||||
|
Specs: []
|
||||||
|
xp: null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend core cart types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type Cart = Core.Cart & {
|
||||||
|
lineItems: Core.LineItem[]
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CartTypes = Core.CartTypes
|
||||||
|
|
||||||
|
export type CartHooks = Core.CartHooks<CartTypes>
|
||||||
|
|
||||||
|
export type GetCartHook = CartHooks['getCart']
|
||||||
|
export type AddItemHook = CartHooks['addItem']
|
||||||
|
export type UpdateItemHook = CartHooks['updateItem']
|
||||||
|
export type RemoveItemHook = CartHooks['removeItem']
|
||||||
|
|
||||||
|
export type CartSchema = Core.CartSchema<CartTypes>
|
||||||
|
|
||||||
|
export type CartHandlers = Core.CartHandlers<CartTypes>
|
||||||
|
|
||||||
|
export type GetCartHandler = CartHandlers['getCart']
|
||||||
|
export type AddItemHandler = CartHandlers['addItem']
|
||||||
|
export type UpdateItemHandler = CartHandlers['updateItem']
|
||||||
|
export type RemoveItemHandler = CartHandlers['removeItem']
|
@ -3,6 +3,8 @@ interface RawVariantSpec {
|
|||||||
Name: string
|
Name: string
|
||||||
OptionID: string
|
OptionID: string
|
||||||
Value: string
|
Value: string
|
||||||
|
PriceMarkupType: string
|
||||||
|
PriceMarkup: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RawSpec {
|
export interface RawSpec {
|
||||||
|
@ -29,7 +29,7 @@ export function normalize(product: RawProduct): Product {
|
|||||||
}))
|
}))
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
id: product.ID,
|
id: '',
|
||||||
options: [],
|
options: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user