diff --git a/framework/bigcommerce/api/cart/index.ts b/framework/bigcommerce/api/cart/index.ts index 043097654..091cabf5a 100644 --- a/framework/bigcommerce/api/cart/index.ts +++ b/framework/bigcommerce/api/cart/index.ts @@ -1,8 +1,9 @@ -import { GetAPISchema } from '@commerce/api' +import type { GetAPISchema } from '@commerce/api' +import type { AddItemOperation } from '@commerce/types' import getCart from './get-cart' import addItem from './add-item' -import updateItem from './handlers/update-item' -import removeItem from './handlers/remove-item' +import updateItem from './update-item' +import removeItem from './remove-item' import type { GetCartHandlerBody, AddCartItemHandlerBody, @@ -18,14 +19,10 @@ export type CartAPI = GetAPISchema< endpoint: { options: {} operations: { - getCart: { - data: Cart | null - body: GetCartHandlerBody - options: { yay: string } - } - addItem: { data: Cart; body: AddCartItemHandlerBody; options: {} } - updateItem: { data: Cart; body: UpdateCartItemHandlerBody; options: {} } - removeItem: { data: Cart; body: RemoveCartItemHandlerBody; options: {} } + getCart: { data: Cart | null; body: GetCartHandlerBody } + addItem: { data: Cart; body: AddItemOperation['body'] } + updateItem: { data: Cart; body: UpdateCartItemHandlerBody } + removeItem: { data: Cart; body: RemoveCartItemHandlerBody } } } } @@ -33,4 +30,4 @@ export type CartAPI = GetAPISchema< export type CartEndpoint = CartAPI['endpoint'] -export const operations = { getCart, addItem } +export const operations = { getCart, addItem, updateItem, removeItem } diff --git a/framework/bigcommerce/api/cart/remove-item.ts b/framework/bigcommerce/api/cart/remove-item.ts new file mode 100644 index 000000000..18c641260 --- /dev/null +++ b/framework/bigcommerce/api/cart/remove-item.ts @@ -0,0 +1,34 @@ +import { normalizeCart } from '@framework/lib/normalize' +import getCartCookie from '../utils/get-cart-cookie' +import type { CartEndpoint } from '.' + +const removeItem: CartEndpoint['operations']['removeItem'] = async ({ + res, + body: { cartId, itemId }, + config, +}) => { + if (!cartId || !itemId) { + return res.status(400).json({ + data: null, + errors: [{ message: 'Invalid request' }], + }) + } + + const result = await config.storeApiFetch<{ data: any } | null>( + `/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`, + { method: 'DELETE' } + ) + const data = result?.data ?? null + + res.setHeader( + 'Set-Cookie', + data + ? // Update the cart cookie + getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge) + : // Remove the cart cookie if the cart was removed (empty items) + getCartCookie(config.cartCookie) + ) + res.status(200).json({ data: normalizeCart(data) }) +} + +export default removeItem diff --git a/framework/bigcommerce/api/cart/update-item.ts b/framework/bigcommerce/api/cart/update-item.ts new file mode 100644 index 000000000..b283e24a3 --- /dev/null +++ b/framework/bigcommerce/api/cart/update-item.ts @@ -0,0 +1,36 @@ +import { normalizeCart } from '@framework/lib/normalize' +import { parseCartItem } from '../utils/parse-item' +import getCartCookie from '../utils/get-cart-cookie' +import type { CartEndpoint } from '.' + +const updateItem: CartEndpoint['operations']['updateItem'] = async ({ + res, + body: { cartId, itemId, item }, + config, +}) => { + 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}?include=line_items.physical_items.options`, + { + method: 'PUT', + body: JSON.stringify({ + line_item: parseCartItem(item), + }), + } + ) + + // Update the cart cookie + res.setHeader( + 'Set-Cookie', + getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge) + ) + res.status(200).json({ data: normalizeCart(data) }) +} + +export default updateItem diff --git a/framework/bigcommerce/api/index.ts b/framework/bigcommerce/api/index.ts index d11f168b8..ebfca8890 100644 --- a/framework/bigcommerce/api/index.ts +++ b/framework/bigcommerce/api/index.ts @@ -1,3 +1,4 @@ +import type { NextApiHandler } from 'next' import type { RequestInit } from '@vercel/fetch' import { CommerceAPI as CoreCommerceAPI, @@ -6,6 +7,8 @@ import { import fetchGraphqlApi from './utils/fetch-graphql-api' import fetchStoreApi from './utils/fetch-store-api' +import type { CartAPI } from './cart' + export interface BigcommerceConfig extends CommerceAPIConfig { // Indicates if the returned metadata with translations should be applied to the // data or returned as it is @@ -104,11 +107,20 @@ export const provider = { export type Provider = typeof provider -export class CommerceAPI< - P extends Provider = Provider -> extends CoreCommerceAPI

{ - constructor(readonly provider: P = provider) { - super(provider) +export type APIs = CartAPI + +export class CommerceAPI extends CoreCommerceAPI { + constructor(customProvider: Provider = provider) { + super(customProvider) + } + + endpoint( + context: E['endpoint'] & { + config?: Provider['config'] + options?: E['schema']['endpoint']['options'] + } + ): NextApiHandler { + return this.endpoint(context) } } diff --git a/framework/bigcommerce/types.ts b/framework/bigcommerce/types.ts index d0d711f3d..beeab0223 100644 --- a/framework/bigcommerce/types.ts +++ b/framework/bigcommerce/types.ts @@ -25,7 +25,6 @@ export type BigcommerceCart = { export type Cart = Core.Cart & { lineItems: LineItem[] - core: string } export type LineItem = Core.LineItem diff --git a/framework/commerce/api/index.ts b/framework/commerce/api/index.ts index c8d1d78f7..1d200fc82 100644 --- a/framework/commerce/api/index.ts +++ b/framework/commerce/api/index.ts @@ -1,19 +1,7 @@ import type { NextApiHandler } from 'next' import type { RequestInit, Response } from '@vercel/fetch' import type { APIEndpoint, APIHandler } from './utils/types' -import type { Cart } from '../types' - -export type CartSchema = { - endpoint: { - options: {} - operations: { - getCart: { data?: Cart | null; body?: any } - addItem: { data?: Cart; body?: any } - updateItem: { data?: Cart; body?: any } - removeItem: { data?: Cart; body?: any } - } - } -} +import type { CartSchema } from '../types' export type APISchemas = CartSchema @@ -78,10 +66,10 @@ export class CommerceAPI

{ Object.assign(this.provider.config, newConfig) } - endpoint>( - context: E['endpoint'] & { + endpoint>( + context: T['endpoint'] & { config?: P['config'] - options?: E['schema']['endpoint']['options'] + options?: T['schema']['endpoint']['options'] } ): NextApiHandler { const commerce = this @@ -93,7 +81,7 @@ export class CommerceAPI

{ res, commerce, config: cfg, - handlers: context.operations, + operations: context.operations, options: context.options ?? {}, }) } diff --git a/framework/commerce/api/utils/types.ts b/framework/commerce/api/utils/types.ts index 27a95df40..41055b606 100644 --- a/framework/commerce/api/utils/types.ts +++ b/framework/commerce/api/utils/types.ts @@ -20,7 +20,7 @@ export type APIHandlerContext< res: NextApiResponse> commerce: C config: C['provider']['config'] - handlers: H + operations: H /** * Custom configs that may be used by a particular handler */ diff --git a/framework/commerce/types/cart.ts b/framework/commerce/types/cart.ts new file mode 100644 index 000000000..f2873b005 --- /dev/null +++ b/framework/commerce/types/cart.ts @@ -0,0 +1,147 @@ +import type { Discount, Measurement, Image } from './common' + +export type LineItem = { + id: string + variantId: string + productId: string + name: string + quantity: number + discounts: Discount[] + // A human-friendly unique string automatically generated from the product’s name + path: string + variant: ProductVariant +} + +export type ProductVariant = { + id: string + // The SKU (stock keeping unit) associated with the product variant. + sku: string + // The product variant’s title, or the product's name. + name: string + // Whether a customer needs to provide a shipping address when placing + // an order for the product variant. + requiresShipping: boolean + // The product variant’s price after all discounts are applied. + price: number + // Product variant’s price, as quoted by the manufacturer/distributor. + listPrice: number + // Image associated with the product variant. Falls back to the product image + // if no image is available. + image?: Image + // Indicates whether this product variant is in stock. + isInStock?: boolean + // Indicates if the product variant is available for sale. + availableForSale?: boolean + // The variant's weight. If a weight was not explicitly specified on the + // variant this will be the product's weight. + weight?: Measurement + // The variant's height. If a height was not explicitly specified on the + // variant, this will be the product's height. + height?: Measurement + // The variant's width. If a width was not explicitly specified on the + // variant, this will be the product's width. + width?: Measurement + // The variant's depth. If a depth was not explicitly specified on the + // variant, this will be the product's depth. + depth?: Measurement +} + +// Shopping cart, a.k.a Checkout +export type Cart = { + id: string + // ID of the customer to which the cart belongs. + customerId?: string + // The email assigned to this cart + email?: string + // The date and time when the cart was created. + createdAt: string + // The currency used for this cart + currency: { code: string } + // Specifies if taxes are included in the line items. + taxesIncluded: boolean + lineItems: LineItem[] + // The sum of all the prices of all the items in the cart. + // Duties, taxes, shipping and discounts excluded. + lineItemsSubtotalPrice: number + // Price of the cart before duties, shipping and taxes. + subtotalPrice: number + // The sum of all the prices of all the items in the cart. + // Duties, taxes and discounts included. + totalPrice: number + // Discounts that have been applied on the cart. + discounts?: Discount[] +} + +/** + * Base cart item body used for cart mutations + */ +export type CartItemBody = { + variantId: string + productId?: string + quantity?: number +} + +/** + * Hooks schema + */ + +export type CartHooks = { + getCart: GetCartHook + addItem: AddItemHook + updateItem: UpdateItemHook + remoteItem: RemoveItemHook +} + +export type GetCartHook = { + data: Cart | null +} + +export type AddItemHook = { + data: Cart + body: { item: CartItemBody } +} + +export type UpdateItemHook = { + data: Cart + body: { itemId: string; item: CartItemBody } +} + +export type RemoveItemHook = { + data: Cart | null + body: { itemId: string } +} + +/** + * API Schema + */ + +export type CartSchema = { + endpoint: { + options: {} + operations: CartOperations + } +} + +export type CartOperations = { + getCart: GetCartOperation + addItem: AddItemOperation + updateItem: UpdateItemOperation + removeItem: RemoveItemOperation +} + +export type GetCartOperation = { + data: Cart | null + body: { cartId?: string } +} + +export type AddItemOperation = AddItemHook & { + body: { cartId: string } +} + +export type UpdateItemOperation = UpdateItemHook & { + body: { cartId: string } +} + +export type RemoveItemOperation = RemoveItemHook & { + body: { cartId: string } +} diff --git a/framework/commerce/types/common.ts b/framework/commerce/types/common.ts new file mode 100644 index 000000000..06908c464 --- /dev/null +++ b/framework/commerce/types/common.ts @@ -0,0 +1,16 @@ +export type Discount = { + // The value of the discount, can be an amount or percentage + value: number +} + +export type Measurement = { + value: number + unit: 'KILOGRAMS' | 'GRAMS' | 'POUNDS' | 'OUNCES' +} + +export type Image = { + url: string + altText?: string + width?: number + height?: number +} diff --git a/framework/commerce/types/index.ts b/framework/commerce/types/index.ts new file mode 100644 index 000000000..057b4c420 --- /dev/null +++ b/framework/commerce/types/index.ts @@ -0,0 +1,130 @@ +import type { Wishlist as BCWishlist } from '../../bigcommerce/api/wishlist' +import type { Customer as BCCustomer } from '../../bigcommerce/api/customers' +import type { SearchProductsData as BCSearchProductsData } from '../../bigcommerce/api/catalog/products' + +export * from './cart' +export * from './common' + +// TODO: Properly define this type +export interface Wishlist extends BCWishlist {} + +// TODO: Properly define this type +export interface Customer extends BCCustomer {} + +// TODO: Properly define this type +export interface SearchProductsData extends BCSearchProductsData {} + +/** + * Cart mutations + */ + +// Base cart item body used for cart mutations +export type CartItemBody = { + variantId: string + productId?: string + quantity?: number +} + +// Body used by the `getCart` operation handler +export type GetCartHandlerBody = { + cartId?: string +} + +// Body used by the add item to cart operation +export type AddCartItemBody = { + item: T +} + +// Body expected by the add item to cart operation handler +export type AddCartItemHandlerBody = Partial< + AddCartItemBody +> & { + cartId?: string +} + +// Body used by the update cart item operation +export type UpdateCartItemBody = { + itemId: string + item: T +} + +// Body expected by the update cart item operation handler +export type UpdateCartItemHandlerBody = Partial< + UpdateCartItemBody +> & { + cartId?: string +} + +// Body used by the remove cart item operation +export type RemoveCartItemBody = { + itemId: string +} + +// Body expected by the remove cart item operation handler +export type RemoveCartItemHandlerBody = Partial & { + cartId?: string +} + +/** + * Temporal types + */ + +interface Entity { + id: string | number + [prop: string]: any +} + +export interface Product2 { + id: string + name: string + description: string + sku?: string + slug?: string + path?: string + images: ProductImage[] + variants: ProductVariant2[] + price: ProductPrice + options: ProductOption[] +} + +export interface Product extends Entity { + name: string + description: string + slug?: string + path?: string + images: ProductImage[] + variants: ProductVariant2[] + price: ProductPrice + options: ProductOption[] + sku?: string +} + +interface ProductOption extends Entity { + displayName: string + values: ProductOptionValues[] +} + +interface ProductOptionValues { + label: string + hexColors?: string[] +} + +interface ProductImage { + url: string + alt?: string +} + +interface ProductVariant2 { + id: string | number + options: ProductOption[] +} + +interface ProductPrice { + value: number + currencyCode: 'USD' | 'ARS' | string | undefined + retailPrice?: number + salePrice?: number + listPrice?: number + extendedSalePrice?: number + extendedListPrice?: number +} diff --git a/pages/api/bigcommerce/cart.ts b/pages/api/bigcommerce/cart.ts index ee9e1e04c..4bb74cb2f 100644 --- a/pages/api/bigcommerce/cart.ts +++ b/pages/api/bigcommerce/cart.ts @@ -1,5 +1,8 @@ import cart from '@commerce/api/endpoints/cart' -import { operations } from '@framework/api/cart' +import { CartAPI, operations } from '@framework/api/cart' import commerce from '@lib/api/commerce' -export default commerce.endpoint({ handler: cart, operations }) +export default commerce.endpoint({ + handler: cart as CartAPI['endpoint']['handler'], + operations, +})