import { serialize, CookieSerializeOptions } from 'cookie' import isAllowedMethod from './utils/is-allowed-method' import createApiHandler, { BigcommerceApiHandler, } from './utils/create-api-handler' import { BigcommerceApiError } from './utils/errors' type Body = Partial | undefined export type ItemBody = { productId: number variantId: number quantity?: number } export type AddItemBody = { item: ItemBody } export type UpdateItemBody = { itemId: string; item: ItemBody } export type RemoveItemBody = { itemId: string } // TODO: this type should match: // https://developer.bigcommerce.com/api-reference/cart-checkout/server-server-cart-api/cart/getacart#responses export type Cart = { id: string parent_id?: string customer_id: number email: string currency: { code: string } tax_included: boolean base_amount: number discount_amount: number cart_amount: number line_items: { custom_items: any[] digital_items: any[] gift_certificates: any[] psysical_items: any[] } // TODO: add missing fields } const METHODS = ['GET', 'POST', 'PUT', 'DELETE'] // TODO: a complete implementation should have schema validation for `req.body` const cartApi: BigcommerceApiHandler = async (req, res, config) => { if (!isAllowedMethod(req, res, METHODS)) return const { cookies } = req const cartId = cookies[config.cartCookie] try { // Return current cart info if (req.method === 'GET') { let result: { data?: Cart } = {} try { result = await config.storeApiFetch( `/v3/carts/${cartId}?include=redirect_urls` ) } catch (error) { if (error instanceof BigcommerceApiError && error.status === 404) { // Remove the cookie if it exists but the cart wasn't found res.setHeader('Set-Cookie', getCartCookie(config.cartCookie)) } else { throw error } } return res.status(200).json({ data: result.data ?? null }) } // Create or add an item to the cart if (req.method === 'POST') { const { item } = (req.body as Body) ?? {} if (!item) { return res.status(400).json({ data: null, errors: [{ message: 'Missing item' }], }) } if (!item.quantity) item.quantity = 1 const options = { method: 'POST', body: JSON.stringify({ line_items: [parseItem(item)], }), } const { data } = cartId ? await config.storeApiFetch(`/v3/carts/${cartId}/items`, options) : await config.storeApiFetch('/v3/carts', options) // Create or update the cart cookie res.setHeader( 'Set-Cookie', getCartCookie(config.cartCookie, data.id, config.cartCookieMaxAge) ) return res.status(200).json({ data }) } // Update item in cart if (req.method === 'PUT') { const { itemId, item } = (req.body as Body) ?? {} if (!cartId || !itemId || !item) { return res.status(400).json({ data: null, errors: [{ message: 'Invalid request' }], }) } const { data } = await config.storeApiFetch( `/v3/carts/${cartId}/items/${itemId}`, { method: 'PUT', body: JSON.stringify({ line_items: [parseItem(item)], }), } ) // Update the cart cookie res.setHeader( 'Set-Cookie', getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge) ) return res.status(200).json({ data }) } // Remove an item from the cart if (req.method === 'DELETE') { const { itemId } = (req.body as Body) ?? {} if (!cartId || !itemId) { return res.status(400).json({ data: null, errors: [{ message: 'Invalid request' }], }) } const { data } = await config.storeApiFetch( `/v3/carts/${cartId}/items/${itemId}`, { method: 'DELETE', } ) // Update the cart cookie res.setHeader( 'Set-Cookie', getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge) ) return res.status(200).json({ data }) } } catch (error) { console.error(error) const message = error instanceof BigcommerceApiError ? 'An unexpected error ocurred with the Bigcommerce API' : 'An unexpected error ocurred' res.status(500).json({ data: null, errors: [{ message }] }) } } 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) } const parseItem = (item: ItemBody) => ({ quantity: item.quantity, product_id: item.productId, variant_id: item.variantId, }) export default createApiHandler(cartApi)