Add ordercloud provider (#500)

* Add ordercloud provider

* Fix provider errors

* Make submit checkout optional

* Make submit checkout optional

* Remove nullables when creating endpoint type

* Update readme

* Log checkout error

* Log error

* Save token to cookie

* Update fetch rest

* Use token at checkout

Co-authored-by: Luis Alvarez <luis@vercel.com>
This commit is contained in:
Gonzalo Pozzo
2021-10-05 09:49:01 -03:00
committed by GitHub
parent f9644fecef
commit 3f0c38461b
90 changed files with 2560 additions and 76 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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<OrdercloudAPI, CartSchema>
export type CartEndpoint = CartAPI['endpoint']
export const handlers: CartEndpoint['handlers'] = {
getCart,
addItem,
updateItem,
removeItem,
}
const cartApi = createEndpoint<CartAPI>({
handler: cartEndpoint,
handlers,
})
export default cartApi

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -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

View File

@@ -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<OrdercloudAPI, CheckoutSchema>
export type CheckoutEndpoint = CheckoutAPI['endpoint']
export const handlers: CheckoutEndpoint['handlers'] = {
getCheckout,
submitCheckout,
}
const checkoutApi = createEndpoint<CheckoutAPI>({
handler: checkoutEndpoint,
handlers,
})
export default checkoutApi

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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<OrdercloudAPI, CustomerAddressSchema>
export type CustomerAddressEndpoint = CustomerAddressAPI['endpoint']
export const handlers: CustomerAddressEndpoint['handlers'] = {
getAddresses,
addItem,
updateItem,
removeItem,
}
const customerAddressApi = createEndpoint<CustomerAddressAPI>({
handler: customerAddressEndpoint,
handlers,
})
export default customerAddressApi

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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<OrdercloudAPI, CustomerCardSchema>
export type CustomerCardEndpoint = CustomerCardAPI['endpoint']
export const handlers: CustomerCardEndpoint['handlers'] = {
getCards,
addItem,
updateItem,
removeItem,
}
const customerCardApi = createEndpoint<CustomerCardAPI>({
handler: customerCardEndpoint,
handlers,
})
export default customerCardApi

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -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: <T>(
method: string,
resource: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => Promise<T>
restMiddlewareFetch: <T>(
method: string,
resource: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => Promise<T>
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<P extends Provider = Provider> = CommerceAPI<P | any>
export function getCommerceApi<P extends Provider>(
customProvider: P = provider as any
): OrdercloudAPI<P> {
return commerceApi(customProvider as any)
}

View File

@@ -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<T extends GetAllPagesOperation>({
config,
preview,
}: {
url?: string
config?: Partial<OrdercloudConfig>
preview?: boolean
} = {}): Promise<T['data']> {
return Promise.resolve({
pages: [],
})
}
return getAllPages
}

View File

@@ -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<Provider>) {
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
config,
}: {
config?: Partial<OrdercloudConfig>
} = {}): Promise<T['data']> {
// 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
}

View File

@@ -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<Provider>) {
async function getAllProducts<T extends GetAllProductsOperation>({
config,
}: {
query?: string
variables?: T['variables']
config?: Partial<OrdercloudConfig>
preview?: boolean
} = {}): Promise<T['data']> {
// 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
}

View File

@@ -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<T extends GetPageOperation>(): Promise<T['data']> {
return Promise.resolve({})
}
return getPage
}

View File

@@ -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<Provider>) {
async function getProduct<T extends GetProductOperation>({
config,
variables,
}: {
query?: string
variables?: T['variables']
config?: Partial<OrdercloudConfig>
preview?: boolean
} = {}): Promise<T['data']> {
// Get fetch from the config
const { restBuyerFetch } = commerce.getConfig(config)
// Get a single product
const productPromise = restBuyerFetch<RawProduct>(
'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
}

View File

@@ -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<Provider>) {
async function getSiteInfo<T extends GetSiteInfoOperation>({
config,
}: {
query?: string
variables?: any
config?: Partial<OrdercloudConfig>
preview?: boolean
} = {}): Promise<T['data']> {
// 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
}

View File

@@ -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'

View 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: [],
}
}

View File

@@ -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

View File

@@ -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<string> {
// 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<T>(opts: {
token: string
path: string
method: string
config: OrdercloudConfig
fetchOptions?: Record<string, any>
body?: Record<string, unknown>
}): Promise<T> {
// 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<T>
} catch (error) {
// If response is empty return it as text
return null as unknown as Promise<T>
}
}
export const createMiddlewareFetcher: (
getConfig: () => OrdercloudConfig
) => <T>(
method: string,
path: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => Promise<T> =
(getConfig) =>
async <T>(
method: string,
path: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => {
// 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<T>({
token,
fetchOptions,
method,
config,
path,
body,
})
}
export const createBuyerFetcher: (
getConfig: () => OrdercloudConfig
) => <T>(
method: string,
path: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => Promise<T> =
(getConfig) =>
async <T>(
method: string,
path: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => {
// Get provider config
const config = getConfig()
// If a token was passed, set it on global
if (fetchOptions?.token) {
global.token = fetchOptions.token
}
// Get a token
if (!global.token) {
global.token = await getToken({
baseUrl: config.commerceUrl,
clientId: process.env.ORDERCLOUD_BUYER_CLIENT_ID as string,
})
}
// Return the data and specify the expected type
const data = await fetchData<T>({
token: global.token as string,
fetchOptions,
config,
method,
path,
body,
})
return {
...data,
meta: { token: global.token as string },
}
}