+
+export function CoreCommerceProvider({
provider,
children,
- config,
}: CommerceProps
) {
- if (!config) {
- throw new Error('CommerceProvider requires a valid config object')
- }
-
const providerRef = useRef(provider)
// TODO: Remove the fetcherRef
const fetcherRef = useRef(provider.fetcher)
- // Because the config is an object, if the parent re-renders this provider
- // will re-render every consumer unless we memoize the config
+ // If the parent re-renders this provider will re-render every
+ // consumer unless we memoize the config
+ const { locale, cartCookie } = providerRef.current
const cfg = useMemo(
- () => ({
- providerRef,
- fetcherRef,
- locale: config.locale,
- cartCookie: config.cartCookie,
- }),
- [config.locale, config.cartCookie]
+ () => ({ providerRef, fetcherRef, locale, cartCookie }),
+ [locale, cartCookie]
)
return {children}
}
+export function getCommerceProvider
(provider: P) {
+ return function CommerceProvider({
+ children,
+ ...props
+ }: CommerceProviderProps) {
+ return (
+
+ {children}
+
+ )
+ }
+}
+
export function useCommerce
() {
return useContext(Commerce) as CommerceContextValue
}
diff --git a/framework/commerce/new-provider.md b/framework/commerce/new-provider.md
index 8c2feeab2..6e149a7d4 100644
--- a/framework/commerce/new-provider.md
+++ b/framework/commerce/new-provider.md
@@ -47,6 +47,12 @@ The app imports from the provider directly instead of the core commerce folder (
The provider folder should only depend on `framework/commerce` and dependencies in the main `package.json`. In the future we'll move the `framework` folder to a package that can be shared easily for multiple apps.
+## Updating the list of known providers
+
+Open [./config.js](./config.js) and add the provider name to the list in `PROVIDERS`.
+
+Then, open [/.env.template](/.env.template) and add the provider name in the first line.
+
## Adding the provider hooks
Using BigCommerce as an example. The first thing to do is export a `CommerceProvider` component that includes a `provider` object with all the handlers that can be used for hooks:
diff --git a/framework/commerce/types/checkout.ts b/framework/commerce/types/checkout.ts
index 9e3c7ecfa..d75b63902 100644
--- a/framework/commerce/types/checkout.ts
+++ b/framework/commerce/types/checkout.ts
@@ -1,10 +1,57 @@
-export type CheckoutSchema = {
+import type { UseSubmitCheckout } from '../checkout/use-submit-checkout'
+import type { Address } from './customer/address'
+import type { Card } from './customer/card'
+
+// Index
+export type Checkout = any
+
+export type CheckoutTypes = {
+ card?: Card
+ address?: Address
+ checkout?: Checkout
+ hasPayment?: boolean
+ hasShipping?: boolean
+}
+
+export type SubmitCheckoutHook = {
+ data: T
+ input?: T
+ fetcherInput: T
+ body: { item: T }
+ actionInput: T
+}
+
+export type GetCheckoutHook = {
+ data: T['checkout'] | null
+ input: {}
+ fetcherInput: { cartId?: string }
+ swrState: { isEmpty: boolean }
+ mutations: { submit: UseSubmitCheckout }
+}
+
+export type CheckoutHooks = {
+ submitCheckout?: SubmitCheckoutHook
+ getCheckout: GetCheckoutHook
+}
+
+export type GetCheckoutHandler =
+ GetCheckoutHook & {
+ body: { cartId: string }
+ }
+
+export type SubmitCheckoutHandler =
+ SubmitCheckoutHook & {
+ body: { cartId: string }
+ }
+
+export type CheckoutHandlers = {
+ getCheckout: GetCheckoutHandler
+ submitCheckout?: SubmitCheckoutHandler
+}
+
+export type CheckoutSchema = {
endpoint: {
options: {}
- handlers: {
- checkout: {
- data: null
- }
- }
+ handlers: CheckoutHandlers
}
}
diff --git a/framework/commerce/types/customer/address.ts b/framework/commerce/types/customer/address.ts
new file mode 100644
index 000000000..8dc6ffc0d
--- /dev/null
+++ b/framework/commerce/types/customer/address.ts
@@ -0,0 +1,111 @@
+export interface Address {
+ id: string
+ mask: string
+}
+
+export interface AddressFields {
+ type: string
+ firstName: string
+ lastName: string
+ company: string
+ streetNumber: string
+ apartments: string
+ zipCode: string
+ city: string
+ country: string
+}
+
+export type CustomerAddressTypes = {
+ address?: Address
+ fields: AddressFields
+}
+
+export type GetAddressesHook<
+ T extends CustomerAddressTypes = CustomerAddressTypes
+> = {
+ data: T['address'][] | null
+ input: {}
+ fetcherInput: { cartId?: string }
+ swrState: { isEmpty: boolean }
+}
+
+export type AddItemHook =
+ {
+ data: T['address']
+ input?: T['fields']
+ fetcherInput: T['fields']
+ body: { item: T['fields'] }
+ actionInput: T['fields']
+ }
+
+export type UpdateItemHook<
+ T extends CustomerAddressTypes = CustomerAddressTypes
+> = {
+ data: T['address'] | null
+ input: { item?: T['fields']; wait?: number }
+ fetcherInput: { itemId: string; item: T['fields'] }
+ body: { itemId: string; item: T['fields'] }
+ actionInput: T['fields'] & { id: string }
+}
+
+export type RemoveItemHook<
+ T extends CustomerAddressTypes = CustomerAddressTypes
+> = {
+ data: T['address'] | null
+ input: { item?: T['address'] }
+ fetcherInput: { itemId: string }
+ body: { itemId: string }
+ actionInput: { id: string }
+}
+
+export type CustomerAddressHooks<
+ T extends CustomerAddressTypes = CustomerAddressTypes
+> = {
+ getAddresses: GetAddressesHook
+ addItem: AddItemHook
+ updateItem: UpdateItemHook
+ removeItem: RemoveItemHook
+}
+
+export type AddressHandler<
+ T extends CustomerAddressTypes = CustomerAddressTypes
+> = GetAddressesHook & {
+ body: { cartId?: string }
+}
+
+export type AddItemHandler<
+ T extends CustomerAddressTypes = CustomerAddressTypes
+> = AddItemHook & {
+ body: { cartId: string }
+}
+
+export type UpdateItemHandler<
+ T extends CustomerAddressTypes = CustomerAddressTypes
+> = UpdateItemHook & {
+ data: T['address']
+ body: { cartId: string }
+}
+
+export type RemoveItemHandler<
+ T extends CustomerAddressTypes = CustomerAddressTypes
+> = RemoveItemHook & {
+ body: { cartId: string }
+}
+
+export type CustomerAddressHandlers<
+ T extends CustomerAddressTypes = CustomerAddressTypes
+> = {
+ getAddresses: GetAddressesHook
+ addItem: AddItemHandler
+ updateItem: UpdateItemHandler
+ removeItem: RemoveItemHandler
+}
+
+export type CustomerAddressSchema<
+ T extends CustomerAddressTypes = CustomerAddressTypes
+> = {
+ endpoint: {
+ options: {}
+ handlers: CustomerAddressHandlers
+ }
+}
diff --git a/framework/commerce/types/customer/card.ts b/framework/commerce/types/customer/card.ts
new file mode 100644
index 000000000..e9b220dcc
--- /dev/null
+++ b/framework/commerce/types/customer/card.ts
@@ -0,0 +1,102 @@
+export interface Card {
+ id: string
+ mask: string
+ provider: string
+}
+
+export interface CardFields {
+ cardHolder: string
+ cardNumber: string
+ cardExpireDate: string
+ cardCvc: string
+ firstName: string
+ lastName: string
+ company: string
+ streetNumber: string
+ zipCode: string
+ city: string
+ country: string
+}
+
+export type CustomerCardTypes = {
+ card?: Card
+ fields: CardFields
+}
+
+export type GetCardsHook = {
+ data: T['card'][] | null
+ input: {}
+ fetcherInput: { cartId?: string }
+ swrState: { isEmpty: boolean }
+}
+
+export type AddItemHook = {
+ data: T['card']
+ input?: T['fields']
+ fetcherInput: T['fields']
+ body: { item: T['fields'] }
+ actionInput: T['fields']
+}
+
+export type UpdateItemHook = {
+ data: T['card'] | null
+ input: { item?: T['fields']; wait?: number }
+ fetcherInput: { itemId: string; item: T['fields'] }
+ body: { itemId: string; item: T['fields'] }
+ actionInput: T['fields'] & { id: string }
+}
+
+export type RemoveItemHook = {
+ data: T['card'] | null
+ input: { item?: T['card'] }
+ fetcherInput: { itemId: string }
+ body: { itemId: string }
+ actionInput: { id: string }
+}
+
+export type CustomerCardHooks =
+ {
+ getCards: GetCardsHook
+ addItem: AddItemHook
+ updateItem: UpdateItemHook
+ removeItem: RemoveItemHook
+ }
+
+export type CardsHandler =
+ GetCardsHook & {
+ body: { cartId?: string }
+ }
+
+export type AddItemHandler =
+ AddItemHook & {
+ body: { cartId: string }
+ }
+
+export type UpdateItemHandler =
+ UpdateItemHook & {
+ data: T['card']
+ body: { cartId: string }
+ }
+
+export type RemoveItemHandler =
+ RemoveItemHook & {
+ body: { cartId: string }
+ }
+
+export type CustomerCardHandlers<
+ T extends CustomerCardTypes = CustomerCardTypes
+> = {
+ getCards: GetCardsHook
+ addItem: AddItemHandler
+ updateItem: UpdateItemHandler
+ removeItem: RemoveItemHandler
+}
+
+export type CustomerCardSchema<
+ T extends CustomerCardTypes = CustomerCardTypes
+> = {
+ endpoint: {
+ options: {}
+ handlers: CustomerCardHandlers
+ }
+}
diff --git a/framework/commerce/types/customer.ts b/framework/commerce/types/customer/index.ts
similarity index 87%
rename from framework/commerce/types/customer.ts
rename to framework/commerce/types/customer/index.ts
index ba90acdf4..70c437c29 100644
--- a/framework/commerce/types/customer.ts
+++ b/framework/commerce/types/customer/index.ts
@@ -1,3 +1,6 @@
+export * as Card from "./card"
+export * as Address from "./address"
+
// TODO: define this type
export type Customer = any
diff --git a/framework/commerce/utils/types.ts b/framework/commerce/utils/types.ts
index 751cea4a5..2bca30852 100644
--- a/framework/commerce/utils/types.ts
+++ b/framework/commerce/utils/types.ts
@@ -87,6 +87,8 @@ export type HookSchemaBase = {
export type SWRHookSchemaBase = HookSchemaBase & {
// Custom state added to the response object of SWR
swrState?: {}
+ // Instances of MutationSchemaBase that the hook returns for better DX
+ mutations?: Record['useHook']>>
}
export type MutationSchemaBase = HookSchemaBase & {
@@ -102,7 +104,7 @@ export type SWRHook = {
context: SWRHookContext
): HookFunction<
H['input'] & { swrOptions?: SwrOptions },
- ResponseState & H['swrState']
+ ResponseState & H['swrState'] & H['mutations']
>
fetchOptions: HookFetcherOptions
fetcher?: HookFetcherFn
diff --git a/framework/commerce/utils/use-data.tsx b/framework/commerce/utils/use-data.tsx
index 4fc208bab..fedd14e50 100644
--- a/framework/commerce/utils/use-data.tsx
+++ b/framework/commerce/utils/use-data.tsx
@@ -1,4 +1,4 @@
-import useSWR, { responseInterface } from 'swr'
+import useSWR, { SWRResponse } from 'swr'
import type {
HookSWRInput,
HookFetchInput,
@@ -11,7 +11,7 @@ import type {
import defineProperty from './define-property'
import { CommerceError } from './errors'
-export type ResponseState = responseInterface & {
+export type ResponseState = SWRResponse & {
isLoading: boolean
}
@@ -72,7 +72,7 @@ const useData: UseData = (options, input, fetcherFn, swrOptions) => {
})
}
- return response
+ return response as typeof response & { isLoading: boolean }
}
export default useData
diff --git a/framework/local/api/endpoints/customer/address.ts b/framework/local/api/endpoints/customer/address.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/local/api/endpoints/customer/address.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/local/api/endpoints/customer/card.ts b/framework/local/api/endpoints/customer/card.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/local/api/endpoints/customer/card.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/local/checkout/use-checkout.tsx b/framework/local/checkout/use-checkout.tsx
new file mode 100644
index 000000000..942f85b83
--- /dev/null
+++ b/framework/local/checkout/use-checkout.tsx
@@ -0,0 +1,14 @@
+import { SWRHook } from '@commerce/utils/types'
+import useCheckout, { UseCheckout } from '@commerce/checkout/use-checkout'
+
+export default useCheckout as UseCheckout
+
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher({ input, options, fetch }) {},
+ useHook:
+ ({ useData }) =>
+ async (input) => ({}),
+}
diff --git a/framework/local/commerce.config.json b/framework/local/commerce.config.json
index 5a0376a31..261211527 100644
--- a/framework/local/commerce.config.json
+++ b/framework/local/commerce.config.json
@@ -1,6 +1,9 @@
{
"provider": "local",
"features": {
- "wishlist": false
+ "wishlist": false,
+ "cart": false,
+ "search": false,
+ "customerAuth": false
}
}
diff --git a/framework/local/customer/address/use-add-item.tsx b/framework/local/customer/address/use-add-item.tsx
new file mode 100644
index 000000000..ac9dcd5cf
--- /dev/null
+++ b/framework/local/customer/address/use-add-item.tsx
@@ -0,0 +1,15 @@
+import useAddItem, { UseAddItem } from '@commerce/customer/address/use-add-item'
+import { MutationHook } from '@commerce/utils/types'
+
+export default useAddItem as UseAddItem
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher({ input, options, fetch }) {},
+ useHook:
+ ({ fetch }) =>
+ () =>
+ async () => ({}),
+}
diff --git a/framework/local/customer/card/use-add-item.tsx b/framework/local/customer/card/use-add-item.tsx
new file mode 100644
index 000000000..7e3afa9c5
--- /dev/null
+++ b/framework/local/customer/card/use-add-item.tsx
@@ -0,0 +1,15 @@
+import useAddItem, { UseAddItem } from '@commerce/customer/card/use-add-item'
+import { MutationHook } from '@commerce/utils/types'
+
+export default useAddItem as UseAddItem
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher({ input, options, fetch }) {},
+ useHook:
+ ({ fetch }) =>
+ () =>
+ async () => ({}),
+}
diff --git a/framework/local/index.tsx b/framework/local/index.tsx
index 2ec304f63..dcc713e69 100644
--- a/framework/local/index.tsx
+++ b/framework/local/index.tsx
@@ -1,32 +1,9 @@
-import * as React from 'react'
-import { ReactNode } from 'react'
-import { localProvider } from './provider'
-import {
- CommerceConfig,
- CommerceProvider as CoreCommerceProvider,
- useCommerce as useCoreCommerce,
-} from '@commerce'
+import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
+import { localProvider, LocalProvider } from './provider'
-export const localConfig: CommerceConfig = {
- locale: 'en-us',
- cartCookie: 'session',
-}
+export { localProvider }
+export type { LocalProvider }
-export function CommerceProvider({
- children,
- ...config
-}: {
- children?: ReactNode
- locale: string
-} & Partial) {
- return (
-
- {children}
-
- )
-}
+export const CommerceProvider = getCommerceProvider(localProvider)
-export const useCommerce = () => useCoreCommerce()
+export const useCommerce = () => useCoreCommerce()
diff --git a/framework/local/provider.ts b/framework/local/provider.ts
index e6a2b0a21..53dc7f574 100644
--- a/framework/local/provider.ts
+++ b/framework/local/provider.ts
@@ -9,7 +9,6 @@ import { handler as useLogin } from './auth/use-login'
import { handler as useLogout } from './auth/use-logout'
import { handler as useSignup } from './auth/use-signup'
-export type Provider = typeof localProvider
export const localProvider = {
locale: 'en-us',
cartCookie: 'session',
@@ -19,3 +18,5 @@ export const localProvider = {
products: { useSearch },
auth: { useLogin, useLogout, useSignup },
}
+
+export type LocalProvider = typeof localProvider
diff --git a/framework/ordercloud/.env.template b/framework/ordercloud/.env.template
new file mode 100644
index 000000000..9b33282ba
--- /dev/null
+++ b/framework/ordercloud/.env.template
@@ -0,0 +1,5 @@
+COMMERCE_PROVIDER=ordercloud
+
+ORDERCLOUD_CLIENT_ID=
+ORDERCLOUD_CLIENT_SECRET=
+STRIPE_SECRET=
diff --git a/framework/ordercloud/README.md b/framework/ordercloud/README.md
new file mode 100644
index 000000000..ca1438eae
--- /dev/null
+++ b/framework/ordercloud/README.md
@@ -0,0 +1,3 @@
+# Next.js Ordercloud Provider
+
+Create your own store from [here](https://nextjs.org/commerce)
diff --git a/framework/ordercloud/api/endpoints/cart/add-item.ts b/framework/ordercloud/api/endpoints/cart/add-item.ts
new file mode 100644
index 000000000..28d372bd0
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/cart/add-item.ts
@@ -0,0 +1,99 @@
+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: { restBuyerFetch, cartCookie, tokenCookie },
+}) => {
+ // Return an error if no item is present
+ if (!item) {
+ return res.status(400).json({
+ data: null,
+ errors: [{ message: 'Missing item' }],
+ })
+ }
+
+ // Store token
+ let token
+
+ // Set the quantity if not present
+ if (!item.quantity) item.quantity = 1
+
+ // Create an order if it doesn't exist
+ if (!cartId) {
+ const { ID, meta } = await restBuyerFetch(
+ 'POST',
+ `/orders/Outgoing`,
+ {}
+ ).then((response: { ID: string; meta: { token: string } }) => response)
+
+ // Set the cart id and token
+ cartId = ID
+ token = meta.token
+
+ // Set the cart and token cookie
+ res.setHeader('Set-Cookie', [
+ serialize(tokenCookie, meta.token, {
+ maxAge: 60 * 60 * 24 * 30,
+ expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
+ secure: process.env.NODE_ENV === 'production',
+ path: '/',
+ sameSite: 'lax',
+ }),
+ 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 restBuyerFetch(
+ 'GET',
+ `/me/products/${item.productId}/variants/${item.variantId}`,
+ null,
+ { token }
+ ).then((res: RawVariant) => res.Specs)
+ }
+
+ // Add the item to the order
+ await restBuyerFetch(
+ 'POST',
+ `/orders/Outgoing/${cartId}/lineitems`,
+ {
+ ProductID: item.productId,
+ Quantity: item.quantity,
+ Specs: specs,
+ },
+ { token }
+ )
+
+ // Get cart
+ const [cart, lineItems] = await Promise.all([
+ restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
+ restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
+ token,
+ }).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
diff --git a/framework/ordercloud/api/endpoints/cart/get-cart.ts b/framework/ordercloud/api/endpoints/cart/get-cart.ts
new file mode 100644
index 000000000..7ea077b54
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/cart/get-cart.ts
@@ -0,0 +1,65 @@
+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 ({
+ req,
+ res,
+ body: { cartId },
+ config: { restBuyerFetch, cartCookie, tokenCookie },
+}) => {
+ if (!cartId) {
+ return res.status(400).json({
+ data: null,
+ errors: [{ message: 'Invalid request' }],
+ })
+ }
+
+ try {
+ // Get token from cookies
+ const token = req.cookies[tokenCookie]
+
+ // Get cart
+ const cart = await restBuyerFetch(
+ 'GET',
+ `/orders/Outgoing/${cartId}`,
+ null,
+ { token }
+ )
+
+ // Get line items
+ const lineItems = await restBuyerFetch(
+ 'GET',
+ `/orders/Outgoing/${cartId}/lineitems`,
+ null,
+ { token }
+ ).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 and token cookie
+ res.setHeader('Set-Cookie', [
+ serialize(cartCookie, cartId, {
+ maxAge: -1,
+ path: '/',
+ }),
+ serialize(tokenCookie, cartId, {
+ maxAge: -1,
+ path: '/',
+ }),
+ ])
+
+ // Return empty cart
+ res.status(200).json({ data: null, errors: [] })
+ }
+}
+
+export default getCart
diff --git a/framework/ordercloud/api/endpoints/cart/index.ts b/framework/ordercloud/api/endpoints/cart/index.ts
new file mode 100644
index 000000000..756bce9fe
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/cart/index.ts
@@ -0,0 +1,28 @@
+import type { CartSchema } from '../../../types/cart'
+import type { OrdercloudAPI } from '../..'
+
+import { GetAPISchema, createEndpoint } from '@commerce/api'
+import cartEndpoint from '@commerce/api/endpoints/cart'
+
+import getCart from './get-cart'
+import addItem from './add-item'
+import updateItem from './update-item'
+import removeItem from './remove-item'
+
+export type CartAPI = GetAPISchema
+
+export type CartEndpoint = CartAPI['endpoint']
+
+export const handlers: CartEndpoint['handlers'] = {
+ getCart,
+ addItem,
+ updateItem,
+ removeItem,
+}
+
+const cartApi = createEndpoint({
+ handler: cartEndpoint,
+ handlers,
+})
+
+export default cartApi
diff --git a/framework/ordercloud/api/endpoints/cart/remove-item.ts b/framework/ordercloud/api/endpoints/cart/remove-item.ts
new file mode 100644
index 000000000..ea9c46e4c
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/cart/remove-item.ts
@@ -0,0 +1,45 @@
+import type { CartEndpoint } from '.'
+
+import { formatCart } from '../../utils/cart'
+import { OrdercloudLineItem } from '../../../types/cart'
+
+const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
+ req,
+ res,
+ body: { cartId, itemId },
+ config: { restBuyerFetch, tokenCookie },
+}) => {
+ if (!cartId || !itemId) {
+ return res.status(400).json({
+ data: null,
+ errors: [{ message: 'Invalid request' }],
+ })
+ }
+
+ // Get token from cookies
+ const token = req.cookies[tokenCookie]
+
+ // Remove the item to the order
+ await restBuyerFetch(
+ 'DELETE',
+ `/orders/Outgoing/${cartId}/lineitems/${itemId}`,
+ null,
+ { token }
+ )
+
+ // Get cart
+ const [cart, lineItems] = await Promise.all([
+ restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
+ restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
+ token,
+ }).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
diff --git a/framework/ordercloud/api/endpoints/cart/update-item.ts b/framework/ordercloud/api/endpoints/cart/update-item.ts
new file mode 100644
index 000000000..20113baee
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/cart/update-item.ts
@@ -0,0 +1,63 @@
+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 ({
+ req,
+ res,
+ body: { cartId, itemId, item },
+ config: { restBuyerFetch, tokenCookie },
+}) => {
+ if (!cartId || !itemId || !item) {
+ return res.status(400).json({
+ data: null,
+ errors: [{ message: 'Invalid request' }],
+ })
+ }
+
+ // Get token from cookies
+ const token = req.cookies[tokenCookie]
+
+ // Store specs
+ let specs: RawVariant['Specs'] = []
+
+ // If a variant is present, fetch its specs
+ if (item.variantId) {
+ specs = await restBuyerFetch(
+ 'GET',
+ `/me/products/${item.productId}/variants/${item.variantId}`,
+ null,
+ { token }
+ ).then((res: RawVariant) => res.Specs)
+ }
+
+ // Add the item to the order
+ await restBuyerFetch(
+ 'PATCH',
+ `/orders/Outgoing/${cartId}/lineitems/${itemId}`,
+ {
+ ProductID: item.productId,
+ Quantity: item.quantity,
+ Specs: specs,
+ },
+ { token }
+ )
+
+ // Get cart
+ const [cart, lineItems] = await Promise.all([
+ restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
+ restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
+ token,
+ }).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
diff --git a/framework/ordercloud/api/endpoints/catalog/index.ts b/framework/ordercloud/api/endpoints/catalog/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/catalog/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/ordercloud/api/endpoints/catalog/products.ts b/framework/ordercloud/api/endpoints/catalog/products.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/catalog/products.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/ordercloud/api/endpoints/checkout/get-checkout.ts b/framework/ordercloud/api/endpoints/checkout/get-checkout.ts
new file mode 100644
index 000000000..c0ab1a40d
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/checkout/get-checkout.ts
@@ -0,0 +1,47 @@
+import type { CheckoutEndpoint } from '.'
+
+const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
+ req,
+ res,
+ body: { cartId },
+ config: { restBuyerFetch, tokenCookie },
+}) => {
+ // Return an error if no item is present
+ if (!cartId) {
+ return res.status(400).json({
+ data: null,
+ errors: [{ message: 'Missing cookie' }],
+ })
+ }
+
+ // Get token from cookies
+ const token = req.cookies[tokenCookie]
+
+ // Register credit card
+ const payments = await restBuyerFetch(
+ 'GET',
+ `/orders/Outgoing/${cartId}/payments`,
+ null,
+ { token }
+ ).then((response: { Items: unknown[] }) => response.Items)
+
+ const address = await restBuyerFetch(
+ 'GET',
+ `/orders/Outgoing/${cartId}`,
+ null,
+ { token }
+ ).then(
+ (response: { ShippingAddressID: string }) => response.ShippingAddressID
+ )
+
+ // Return cart and errors
+ res.status(200).json({
+ data: {
+ hasPayment: payments.length > 0,
+ hasShipping: Boolean(address),
+ },
+ errors: [],
+ })
+}
+
+export default getCheckout
diff --git a/framework/ordercloud/api/endpoints/checkout/index.ts b/framework/ordercloud/api/endpoints/checkout/index.ts
new file mode 100644
index 000000000..e1b8a9f1c
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/checkout/index.ts
@@ -0,0 +1,23 @@
+import type { CheckoutSchema } from '../../../types/checkout'
+import type { OrdercloudAPI } from '../..'
+
+import { GetAPISchema, createEndpoint } from '@commerce/api'
+import checkoutEndpoint from '@commerce/api/endpoints/checkout'
+
+import getCheckout from './get-checkout'
+import submitCheckout from './submit-checkout'
+
+export type CheckoutAPI = GetAPISchema
+export type CheckoutEndpoint = CheckoutAPI['endpoint']
+
+export const handlers: CheckoutEndpoint['handlers'] = {
+ getCheckout,
+ submitCheckout,
+}
+
+const checkoutApi = createEndpoint({
+ handler: checkoutEndpoint,
+ handlers,
+})
+
+export default checkoutApi
diff --git a/framework/ordercloud/api/endpoints/checkout/submit-checkout.ts b/framework/ordercloud/api/endpoints/checkout/submit-checkout.ts
new file mode 100644
index 000000000..8cd9be5e4
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/checkout/submit-checkout.ts
@@ -0,0 +1,32 @@
+import type { CheckoutEndpoint } from '.'
+
+const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
+ req,
+ res,
+ body: { cartId },
+ config: { restBuyerFetch, tokenCookie },
+}) => {
+ // Return an error if no item is present
+ if (!cartId) {
+ return res.status(400).json({
+ data: null,
+ errors: [{ message: 'Missing item' }],
+ })
+ }
+
+ // Get token from cookies
+ const token = req.cookies[tokenCookie]
+
+ // Submit order
+ await restBuyerFetch(
+ 'POST',
+ `/orders/Outgoing/${cartId}/submit`,
+ {},
+ { token }
+ )
+
+ // Return cart and errors
+ res.status(200).json({ data: null, errors: [] })
+}
+
+export default submitCheckout
diff --git a/framework/ordercloud/api/endpoints/customer/address/add-item.ts b/framework/ordercloud/api/endpoints/customer/address/add-item.ts
new file mode 100644
index 000000000..434c2400d
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/address/add-item.ts
@@ -0,0 +1,47 @@
+import type { CustomerAddressEndpoint } from '.'
+
+const addItem: CustomerAddressEndpoint['handlers']['addItem'] = async ({
+ res,
+ body: { item, cartId },
+ config: { restBuyerFetch },
+}) => {
+ // Return an error if no item is present
+ if (!item) {
+ return res.status(400).json({
+ data: null,
+ errors: [{ message: 'Missing item' }],
+ })
+ }
+
+ // Return an error if no item is present
+ if (!cartId) {
+ return res.status(400).json({
+ data: null,
+ errors: [{ message: 'Cookie not found' }],
+ })
+ }
+
+ // Register address
+ const address = await restBuyerFetch('POST', `/me/addresses`, {
+ AddressName: 'main address',
+ CompanyName: item.company,
+ FirstName: item.firstName,
+ LastName: item.lastName,
+ Street1: item.streetNumber,
+ Street2: item.streetNumber,
+ City: item.city,
+ State: item.city,
+ Zip: item.zipCode,
+ Country: item.country.slice(0, 2).toLowerCase(),
+ Shipping: true,
+ }).then((response: { ID: string }) => response.ID)
+
+ // Assign address to order
+ await restBuyerFetch('PATCH', `/orders/Outgoing/${cartId}`, {
+ ShippingAddressID: address,
+ })
+
+ return res.status(200).json({ data: null, errors: [] })
+}
+
+export default addItem
diff --git a/framework/ordercloud/api/endpoints/customer/address/get-addresses.ts b/framework/ordercloud/api/endpoints/customer/address/get-addresses.ts
new file mode 100644
index 000000000..2e27591c0
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/address/get-addresses.ts
@@ -0,0 +1,9 @@
+import type { CustomerAddressEndpoint } from '.'
+
+const getCards: CustomerAddressEndpoint['handlers']['getAddresses'] = async ({
+ res,
+}) => {
+ return res.status(200).json({ data: null, errors: [] })
+}
+
+export default getCards
diff --git a/framework/ordercloud/api/endpoints/customer/address/index.ts b/framework/ordercloud/api/endpoints/customer/address/index.ts
new file mode 100644
index 000000000..385bc57f1
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/address/index.ts
@@ -0,0 +1,27 @@
+import type { CustomerAddressSchema } from '../../../../types/customer/address'
+import type { OrdercloudAPI } from '../../..'
+
+import { GetAPISchema, createEndpoint } from '@commerce/api'
+import customerAddressEndpoint from '@commerce/api/endpoints/customer/address'
+
+import getAddresses from './get-addresses'
+import addItem from './add-item'
+import updateItem from './update-item'
+import removeItem from './remove-item'
+
+export type CustomerAddressAPI = GetAPISchema
+export type CustomerAddressEndpoint = CustomerAddressAPI['endpoint']
+
+export const handlers: CustomerAddressEndpoint['handlers'] = {
+ getAddresses,
+ addItem,
+ updateItem,
+ removeItem,
+}
+
+const customerAddressApi = createEndpoint({
+ handler: customerAddressEndpoint,
+ handlers,
+})
+
+export default customerAddressApi
diff --git a/framework/ordercloud/api/endpoints/customer/address/remove-item.ts b/framework/ordercloud/api/endpoints/customer/address/remove-item.ts
new file mode 100644
index 000000000..fba4e1154
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/address/remove-item.ts
@@ -0,0 +1,9 @@
+import type { CustomerAddressEndpoint } from '.'
+
+const removeItem: CustomerAddressEndpoint['handlers']['removeItem'] = async ({
+ res,
+}) => {
+ return res.status(200).json({ data: null, errors: [] })
+}
+
+export default removeItem
diff --git a/framework/ordercloud/api/endpoints/customer/address/update-item.ts b/framework/ordercloud/api/endpoints/customer/address/update-item.ts
new file mode 100644
index 000000000..4c4b4b9ae
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/address/update-item.ts
@@ -0,0 +1,9 @@
+import type { CustomerAddressEndpoint } from '.'
+
+const updateItem: CustomerAddressEndpoint['handlers']['updateItem'] = async ({
+ res,
+}) => {
+ return res.status(200).json({ data: null, errors: [] })
+}
+
+export default updateItem
diff --git a/framework/ordercloud/api/endpoints/customer/card/add-item.ts b/framework/ordercloud/api/endpoints/customer/card/add-item.ts
new file mode 100644
index 000000000..ad7dead7c
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/card/add-item.ts
@@ -0,0 +1,74 @@
+import type { CustomerCardEndpoint } from '.'
+import type { OredercloudCreditCard } from '../../../../types/customer/card'
+
+import Stripe from 'stripe'
+
+const stripe = new Stripe(process.env.STRIPE_SECRET as string, {
+ apiVersion: '2020-08-27',
+})
+
+const addItem: CustomerCardEndpoint['handlers']['addItem'] = async ({
+ res,
+ body: { item, cartId },
+ config: { restBuyerFetch, restMiddlewareFetch },
+}) => {
+ // Return an error if no item is present
+ if (!item) {
+ return res.status(400).json({
+ data: null,
+ errors: [{ message: 'Missing item' }],
+ })
+ }
+
+ // Return an error if no item is present
+ if (!cartId) {
+ return res.status(400).json({
+ data: null,
+ errors: [{ message: 'Cookie not found' }],
+ })
+ }
+
+ // Get token
+ const token = await stripe.tokens
+ .create({
+ card: {
+ number: item.cardNumber,
+ exp_month: item.cardExpireDate.split('/')[0],
+ exp_year: item.cardExpireDate.split('/')[1],
+ cvc: item.cardCvc,
+ },
+ })
+ .then((res: { id: string }) => res.id)
+
+ // Register credit card
+ const creditCard = await restBuyerFetch('POST', `/me/creditcards`, {
+ Token: token,
+ CardType: 'credit',
+ PartialAccountNumber: item.cardNumber.slice(-4),
+ CardholderName: item.cardHolder,
+ ExpirationDate: item.cardExpireDate,
+ }).then((response: OredercloudCreditCard) => response.ID)
+
+ // Assign payment to order
+ const payment = await restBuyerFetch(
+ 'POST',
+ `/orders/All/${cartId}/payments`,
+ {
+ Type: 'CreditCard',
+ CreditCardID: creditCard,
+ }
+ ).then((response: { ID: string }) => response.ID)
+
+ // Accept payment to order
+ await restMiddlewareFetch(
+ 'PATCH',
+ `/orders/All/${cartId}/payments/${payment}`,
+ {
+ Accepted: true,
+ }
+ )
+
+ return res.status(200).json({ data: null, errors: [] })
+}
+
+export default addItem
diff --git a/framework/ordercloud/api/endpoints/customer/card/get-cards.ts b/framework/ordercloud/api/endpoints/customer/card/get-cards.ts
new file mode 100644
index 000000000..e77520803
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/card/get-cards.ts
@@ -0,0 +1,9 @@
+import type { CustomerCardEndpoint } from '.'
+
+const getCards: CustomerCardEndpoint['handlers']['getCards'] = async ({
+ res,
+}) => {
+ return res.status(200).json({ data: null, errors: [] })
+}
+
+export default getCards
diff --git a/framework/ordercloud/api/endpoints/customer/card/index.ts b/framework/ordercloud/api/endpoints/customer/card/index.ts
new file mode 100644
index 000000000..672939a8b
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/card/index.ts
@@ -0,0 +1,27 @@
+import type { CustomerCardSchema } from '../../../../types/customer/card'
+import type { OrdercloudAPI } from '../../..'
+
+import { GetAPISchema, createEndpoint } from '@commerce/api'
+import customerCardEndpoint from '@commerce/api/endpoints/customer/card'
+
+import getCards from './get-cards'
+import addItem from './add-item'
+import updateItem from './update-item'
+import removeItem from './remove-item'
+
+export type CustomerCardAPI = GetAPISchema
+export type CustomerCardEndpoint = CustomerCardAPI['endpoint']
+
+export const handlers: CustomerCardEndpoint['handlers'] = {
+ getCards,
+ addItem,
+ updateItem,
+ removeItem,
+}
+
+const customerCardApi = createEndpoint({
+ handler: customerCardEndpoint,
+ handlers,
+})
+
+export default customerCardApi
diff --git a/framework/ordercloud/api/endpoints/customer/card/remove-item.ts b/framework/ordercloud/api/endpoints/customer/card/remove-item.ts
new file mode 100644
index 000000000..1a81d1cf4
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/card/remove-item.ts
@@ -0,0 +1,9 @@
+import type { CustomerCardEndpoint } from '.'
+
+const removeItem: CustomerCardEndpoint['handlers']['removeItem'] = async ({
+ res,
+}) => {
+ return res.status(200).json({ data: null, errors: [] })
+}
+
+export default removeItem
diff --git a/framework/ordercloud/api/endpoints/customer/card/update-item.ts b/framework/ordercloud/api/endpoints/customer/card/update-item.ts
new file mode 100644
index 000000000..9770644aa
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/card/update-item.ts
@@ -0,0 +1,9 @@
+import type { CustomerCardEndpoint } from '.'
+
+const updateItem: CustomerCardEndpoint['handlers']['updateItem'] = async ({
+ res,
+}) => {
+ return res.status(200).json({ data: null, errors: [] })
+}
+
+export default updateItem
diff --git a/framework/ordercloud/api/endpoints/customer/index.ts b/framework/ordercloud/api/endpoints/customer/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/ordercloud/api/endpoints/login/index.ts b/framework/ordercloud/api/endpoints/login/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/login/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/ordercloud/api/endpoints/logout/index.ts b/framework/ordercloud/api/endpoints/logout/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/logout/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/ordercloud/api/endpoints/signup/index.ts b/framework/ordercloud/api/endpoints/signup/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/signup/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/ordercloud/api/endpoints/wishlist/index.tsx b/framework/ordercloud/api/endpoints/wishlist/index.tsx
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/wishlist/index.tsx
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/ordercloud/api/index.ts b/framework/ordercloud/api/index.ts
new file mode 100644
index 000000000..df62843ab
--- /dev/null
+++ b/framework/ordercloud/api/index.ts
@@ -0,0 +1,71 @@
+import type { CommerceAPI, CommerceAPIConfig } from '@commerce/api'
+import { getCommerceApi as commerceApi } from '@commerce/api'
+import { createBuyerFetcher, createMiddlewareFetcher } from './utils/fetch-rest'
+import createGraphqlFetcher from './utils/fetch-graphql'
+
+import getAllPages from './operations/get-all-pages'
+import getPage from './operations/get-page'
+import getSiteInfo from './operations/get-site-info'
+import getAllProductPaths from './operations/get-all-product-paths'
+import getAllProducts from './operations/get-all-products'
+import getProduct from './operations/get-product'
+
+import {
+ API_URL,
+ API_VERSION,
+ CART_COOKIE,
+ CUSTOMER_COOKIE,
+ TOKEN_COOKIE,
+} from '../constants'
+
+export interface OrdercloudConfig extends CommerceAPIConfig {
+ restBuyerFetch: (
+ method: string,
+ resource: string,
+ body?: Record,
+ fetchOptions?: Record
+ ) => Promise
+ restMiddlewareFetch: (
+ method: string,
+ resource: string,
+ body?: Record,
+ fetchOptions?: Record
+ ) => Promise
+ apiVersion: string
+ tokenCookie: string
+}
+
+const config: OrdercloudConfig = {
+ commerceUrl: API_URL,
+ apiToken: '',
+ apiVersion: API_VERSION,
+ cartCookie: CART_COOKIE,
+ customerCookie: CUSTOMER_COOKIE,
+ tokenCookie: TOKEN_COOKIE,
+ cartCookieMaxAge: 2592000,
+ restBuyerFetch: createBuyerFetcher(() => getCommerceApi().getConfig()),
+ restMiddlewareFetch: createMiddlewareFetcher(() =>
+ getCommerceApi().getConfig()
+ ),
+ fetch: createGraphqlFetcher(() => getCommerceApi().getConfig()),
+}
+
+const operations = {
+ getAllPages,
+ getPage,
+ getSiteInfo,
+ getAllProductPaths,
+ getAllProducts,
+ getProduct,
+}
+
+export const provider = { config, operations }
+
+export type Provider = typeof provider
+export type OrdercloudAPI = CommerceAPI
+
+export function getCommerceApi
(
+ customProvider: P = provider as any
+): OrdercloudAPI
{
+ return commerceApi(customProvider as any)
+}
diff --git a/framework/ordercloud/api/operations/get-all-pages.ts b/framework/ordercloud/api/operations/get-all-pages.ts
new file mode 100644
index 000000000..1727532e2
--- /dev/null
+++ b/framework/ordercloud/api/operations/get-all-pages.ts
@@ -0,0 +1,22 @@
+import type { OrdercloudConfig } from '../'
+
+import { GetAllPagesOperation } from '@commerce/types/page'
+
+export type Page = { url: string }
+export type GetAllPagesResult = { pages: Page[] }
+
+export default function getAllPagesOperation() {
+ async function getAllPages({
+ config,
+ preview,
+ }: {
+ url?: string
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise {
+ return Promise.resolve({
+ pages: [],
+ })
+ }
+ return getAllPages
+}
diff --git a/framework/ordercloud/api/operations/get-all-product-paths.ts b/framework/ordercloud/api/operations/get-all-product-paths.ts
new file mode 100644
index 000000000..1ac23c033
--- /dev/null
+++ b/framework/ordercloud/api/operations/get-all-product-paths.ts
@@ -0,0 +1,34 @@
+import type { OperationContext } from '@commerce/api/operations'
+import type { GetAllProductPathsOperation } from '@commerce/types/product'
+
+import type { RawProduct } from '../../types/product'
+import type { OrdercloudConfig, Provider } from '../'
+
+export type GetAllProductPathsResult = {
+ products: Array<{ path: string }>
+}
+
+export default function getAllProductPathsOperation({
+ commerce,
+}: OperationContext) {
+ async function getAllProductPaths({
+ config,
+ }: {
+ config?: Partial
+ } = {}): Promise {
+ // Get fetch from the config
+ const { restBuyerFetch } = commerce.getConfig(config)
+
+ // Get all products
+ const rawProducts: RawProduct[] = await restBuyerFetch<{
+ Items: RawProduct[]
+ }>('GET', '/me/products').then((response) => response.Items)
+
+ return {
+ // Match a path for every product retrieved
+ products: rawProducts.map((product) => ({ path: `/${product.ID}` })),
+ }
+ }
+
+ return getAllProductPaths
+}
diff --git a/framework/ordercloud/api/operations/get-all-products.ts b/framework/ordercloud/api/operations/get-all-products.ts
new file mode 100644
index 000000000..6af24d945
--- /dev/null
+++ b/framework/ordercloud/api/operations/get-all-products.ts
@@ -0,0 +1,35 @@
+import type { GetAllProductsOperation } from '@commerce/types/product'
+import type { OperationContext } from '@commerce/api/operations'
+
+import type { RawProduct } from '../../types/product'
+import type { OrdercloudConfig, Provider } from '../index'
+
+import { normalize as normalizeProduct } from '../../utils/product'
+
+export default function getAllProductsOperation({
+ commerce,
+}: OperationContext) {
+ async function getAllProducts({
+ config,
+ }: {
+ query?: string
+ variables?: T['variables']
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise {
+ // Get fetch from the config
+ const { restBuyerFetch } = commerce.getConfig(config)
+
+ // Get all products
+ const rawProducts: RawProduct[] = await restBuyerFetch<{
+ Items: RawProduct[]
+ }>('GET', '/me/products').then((response) => response.Items)
+
+ return {
+ // Normalize products to commerce schema
+ products: rawProducts.map(normalizeProduct),
+ }
+ }
+
+ return getAllProducts
+}
diff --git a/framework/ordercloud/api/operations/get-page.ts b/framework/ordercloud/api/operations/get-page.ts
new file mode 100644
index 000000000..6b0a86a4d
--- /dev/null
+++ b/framework/ordercloud/api/operations/get-page.ts
@@ -0,0 +1,15 @@
+import { GetPageOperation } from "@commerce/types/page"
+
+export type Page = any
+export type GetPageResult = { page?: Page }
+
+export type PageVariables = {
+ id: number
+}
+
+export default function getPageOperation() {
+ async function getPage(): Promise {
+ return Promise.resolve({})
+ }
+ return getPage
+}
diff --git a/framework/ordercloud/api/operations/get-product.ts b/framework/ordercloud/api/operations/get-product.ts
new file mode 100644
index 000000000..864f931d4
--- /dev/null
+++ b/framework/ordercloud/api/operations/get-product.ts
@@ -0,0 +1,60 @@
+import type { OperationContext } from '@commerce/api/operations'
+import type { GetProductOperation } from '@commerce/types/product'
+
+import type { RawProduct, RawSpec, RawVariant } from '../../types/product'
+import type { OrdercloudConfig, Provider } from '../index'
+
+import { normalize as normalizeProduct } from '../../utils/product'
+
+export default function getProductOperation({
+ commerce,
+}: OperationContext) {
+ async function getProduct({
+ config,
+ variables,
+ }: {
+ query?: string
+ variables?: T['variables']
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise {
+ // Get fetch from the config
+ const { restBuyerFetch } = commerce.getConfig(config)
+
+ // Get a single product
+ const productPromise = restBuyerFetch(
+ 'GET',
+ `/me/products/${variables?.slug}`
+ )
+
+ // Get product specs
+ const specsPromise = restBuyerFetch<{ Items: RawSpec[] }>(
+ 'GET',
+ `/me/products/${variables?.slug}/specs`
+ ).then((res) => res.Items)
+
+ // Get product variants
+ const variantsPromise = restBuyerFetch<{ Items: RawVariant[] }>(
+ 'GET',
+ `/me/products/${variables?.slug}/variants`
+ ).then((res) => res.Items)
+
+ // Execute all promises in parallel
+ const [product, specs, variants] = await Promise.all([
+ productPromise,
+ specsPromise,
+ variantsPromise,
+ ])
+
+ // Hydrate product
+ product.xp.Specs = specs
+ product.xp.Variants = variants
+
+ return {
+ // Normalize product to commerce schema
+ product: normalizeProduct(product),
+ }
+ }
+
+ return getProduct
+}
diff --git a/framework/ordercloud/api/operations/get-site-info.ts b/framework/ordercloud/api/operations/get-site-info.ts
new file mode 100644
index 000000000..95188c58e
--- /dev/null
+++ b/framework/ordercloud/api/operations/get-site-info.ts
@@ -0,0 +1,46 @@
+import type { OperationContext } from '@commerce/api/operations'
+import type { Category, GetSiteInfoOperation } from '@commerce/types/site'
+
+import type { RawCategory } from '../../types/category'
+import type { OrdercloudConfig, Provider } from '../index'
+
+export type GetSiteInfoResult<
+ T extends { categories: any[]; brands: any[] } = {
+ categories: Category[]
+ brands: any[]
+ }
+> = T
+
+export default function getSiteInfoOperation({
+ commerce,
+}: OperationContext) {
+ async function getSiteInfo({
+ config,
+ }: {
+ query?: string
+ variables?: any
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise {
+ // Get fetch from the config
+ const { restBuyerFetch } = commerce.getConfig(config)
+
+ // Get list of categories
+ const rawCategories: RawCategory[] = await restBuyerFetch<{
+ Items: RawCategory[]
+ }>('GET', `/me/categories`).then((response) => response.Items)
+
+ return {
+ // Normalize categories
+ categories: rawCategories.map((category) => ({
+ id: category.ID,
+ name: category.Name,
+ slug: category.ID,
+ path: `/${category.ID}`,
+ })),
+ brands: [],
+ }
+ }
+
+ return getSiteInfo
+}
diff --git a/framework/ordercloud/api/operations/index.ts b/framework/ordercloud/api/operations/index.ts
new file mode 100644
index 000000000..84b04a978
--- /dev/null
+++ b/framework/ordercloud/api/operations/index.ts
@@ -0,0 +1,6 @@
+export { default as getAllPages } from './get-all-pages'
+export { default as getPage } from './get-page'
+export { default as getSiteInfo } from './get-site-info'
+export { default as getProduct } from './get-product'
+export { default as getAllProducts } from './get-all-products'
+export { default as getAllProductPaths } from './get-all-product-paths'
diff --git a/framework/ordercloud/api/utils/cart.ts b/framework/ordercloud/api/utils/cart.ts
new file mode 100644
index 000000000..716f3521e
--- /dev/null
+++ b/framework/ordercloud/api/utils/cart.ts
@@ -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: [],
+ }
+}
diff --git a/framework/ordercloud/api/utils/fetch-graphql.ts b/framework/ordercloud/api/utils/fetch-graphql.ts
new file mode 100644
index 000000000..af72a337c
--- /dev/null
+++ b/framework/ordercloud/api/utils/fetch-graphql.ts
@@ -0,0 +1,14 @@
+import type { GraphQLFetcher } from '@commerce/api'
+import type { OrdercloudConfig } from '../'
+
+import { FetcherError } from '@commerce/utils/errors'
+
+const fetchGraphqlApi: (getConfig: () => OrdercloudConfig) => GraphQLFetcher =
+ () => async () => {
+ throw new FetcherError({
+ errors: [{ message: 'GraphQL fetch is not implemented' }],
+ status: 500,
+ })
+ }
+
+export default fetchGraphqlApi
diff --git a/framework/ordercloud/api/utils/fetch-rest.ts b/framework/ordercloud/api/utils/fetch-rest.ts
new file mode 100644
index 000000000..fd686b958
--- /dev/null
+++ b/framework/ordercloud/api/utils/fetch-rest.ts
@@ -0,0 +1,176 @@
+import vercelFetch from '@vercel/fetch'
+import { FetcherError } from '@commerce/utils/errors'
+
+import { OrdercloudConfig } from '../index'
+
+// Get an instance to vercel fetch
+const fetch = vercelFetch()
+
+// Get token util
+async function getToken({
+ baseUrl,
+ clientId,
+ clientSecret,
+}: {
+ baseUrl: string
+ clientId: string
+ clientSecret?: string
+}): Promise {
+ // If not, get a new one and store it
+ const authResponse = await fetch(`${baseUrl}/oauth/token`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ Accept: 'application/json',
+ },
+ body: `client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`,
+ })
+
+ // 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,
+ })
+ }
+
+ // Return the token
+ return authResponse
+ .json()
+ .then((response: { access_token: string }) => response.access_token)
+}
+
+export async function fetchData(opts: {
+ token: string
+ path: string
+ method: string
+ config: OrdercloudConfig
+ fetchOptions?: Record
+ body?: Record
+}): Promise {
+ // Destructure opts
+ const { path, body, fetchOptions, config, token, method = 'GET' } = opts
+
+ // Do the request with the correct headers
+ const dataResponse = await fetch(
+ `${config.commerceUrl}/${config.apiVersion}${path}`,
+ {
+ ...fetchOptions,
+ method,
+ headers: {
+ ...fetchOptions?.headers,
+ 'Content-Type': 'application/json',
+ accept: 'application/json, text/plain, */*',
+ authorization: `Bearer ${token}`,
+ },
+ body: body ? JSON.stringify(body) : undefined,
+ }
+ )
+
+ // If something failed getting the data response
+ if (!dataResponse.ok) {
+ // Get the body of it
+ const error = await dataResponse.textConverted()
+
+ // And return an error
+ throw new FetcherError({
+ errors: [{ message: error || dataResponse.statusText }],
+ status: dataResponse.status,
+ })
+ }
+
+ try {
+ // Return data response as json
+ return (await dataResponse.json()) as Promise
+ } catch (error) {
+ // If response is empty return it as text
+ return null as unknown as Promise
+ }
+}
+
+export const createMiddlewareFetcher: (
+ getConfig: () => OrdercloudConfig
+) => (
+ method: string,
+ path: string,
+ body?: Record,
+ fetchOptions?: Record
+) => Promise =
+ (getConfig) =>
+ async (
+ method: string,
+ path: string,
+ body?: Record,
+ fetchOptions?: Record
+ ) => {
+ // Get provider config
+ const config = getConfig()
+
+ // Get a token
+ const token = await getToken({
+ baseUrl: config.commerceUrl,
+ clientId: process.env.ORDERCLOUD_MIDDLEWARE_CLIENT_ID as string,
+ clientSecret: process.env.ORDERCLOUD_MIDDLEWARE_CLIENT_SECRET,
+ })
+
+ // Return the data and specify the expected type
+ return fetchData({
+ token,
+ fetchOptions,
+ method,
+ config,
+ path,
+ body,
+ })
+ }
+
+export const createBuyerFetcher: (
+ getConfig: () => OrdercloudConfig
+) => (
+ method: string,
+ path: string,
+ body?: Record,
+ fetchOptions?: Record
+) => Promise =
+ (getConfig) =>
+ async (
+ method: string,
+ path: string,
+ body?: Record,
+ fetchOptions?: Record