mirror of
https://github.com/vercel/commerce.git
synced 2025-07-23 04:36:49 +00:00
Dynamic API routes (#836)
* Add dynamic API endpoints * Add missing dependency * Update api handlers * Updates * Fix build errors * Update package.json * Add checkout endpoint parser & update errors * Update tsconfig.json * Update cart.ts * Update parser * Update errors.ts * Update errors.ts * Move to Edge runtime * Revert to local * Fix switchable runtimes * Make nodejs default runtime * Update pnpm-lock.yaml * Update handlers * Fix build errors * Change headers
This commit is contained in:
@@ -48,10 +48,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/commerce": "workspace:*",
|
||||
"@vercel/fetch": "^6.2.0",
|
||||
"stripe": "^8.197.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"node-fetch": "^2.6.7",
|
||||
"cookie": "^0.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@@ -1,72 +1,65 @@
|
||||
import type { CartEndpoint } from '.'
|
||||
import type { RawVariant } from '../../../types/product'
|
||||
import type { LineItem } from '@vercel/commerce/types/cart'
|
||||
|
||||
import { serialize } from 'cookie'
|
||||
import type { RawVariantSpec } from '../../../types/product'
|
||||
|
||||
import { formatCart } from '../../utils/cart'
|
||||
import { serialize } from 'cookie'
|
||||
|
||||
const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
||||
res,
|
||||
req,
|
||||
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
|
||||
// Get token
|
||||
let token = req.cookies.get(tokenCookie)
|
||||
let headers: any = {}
|
||||
|
||||
// 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)
|
||||
{},
|
||||
{ token }
|
||||
)
|
||||
|
||||
// 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',
|
||||
}),
|
||||
])
|
||||
headers = {
|
||||
'set-cookie': [
|
||||
serialize(cartCookie, cartId!, {
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
if (meta?.token) {
|
||||
headers['set-cookie'].push(
|
||||
serialize(tokenCookie, meta.token?.access_token, {
|
||||
maxAge: meta.token.expires_in,
|
||||
expires: new Date(Date.now() + meta.token.expires_in * 1000),
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Store specs
|
||||
let specs: RawVariant['Specs'] = []
|
||||
let specs: RawVariantSpec[] = []
|
||||
|
||||
// If a variant is present, fetch its specs
|
||||
if (item.variantId) {
|
||||
specs = await restBuyerFetch(
|
||||
if (item.variantId !== 'undefined') {
|
||||
const { Specs } = await restBuyerFetch(
|
||||
'GET',
|
||||
`/me/products/${item.productId}/variants/${item.variantId}`,
|
||||
null,
|
||||
{ token }
|
||||
).then((res: RawVariant) => res.Specs)
|
||||
)
|
||||
specs = Specs
|
||||
}
|
||||
|
||||
// Add the item to the order
|
||||
@@ -81,19 +74,19 @@ const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
||||
{ token }
|
||||
)
|
||||
|
||||
// Get cart
|
||||
const [cart, lineItems] = await Promise.all([
|
||||
// Get cart & line items
|
||||
const [cart, { Items }] = await Promise.all([
|
||||
restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
|
||||
restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
|
||||
token,
|
||||
}).then((response: { Items: LineItem[] }) => response.Items),
|
||||
}),
|
||||
])
|
||||
|
||||
// Format cart
|
||||
const formattedCart = formatCart(cart, lineItems)
|
||||
const formattedCart = formatCart(cart, Items)
|
||||
|
||||
// Return cart and errors
|
||||
res.status(200).json({ data: formattedCart, errors: [] })
|
||||
// Return cart and headers
|
||||
return { data: formattedCart, headers }
|
||||
}
|
||||
|
||||
export default addItem
|
||||
|
@@ -1,64 +1,56 @@
|
||||
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 no cartId is provided, return data null
|
||||
if (!cartId) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Invalid request' }],
|
||||
})
|
||||
return { data: null }
|
||||
}
|
||||
|
||||
try {
|
||||
// Get token from cookies
|
||||
const token = req.cookies[tokenCookie]
|
||||
// Get token
|
||||
const token = req.cookies.get(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: '/',
|
||||
// Get cart & line items
|
||||
const [cart, { Items }] = await Promise.all([
|
||||
restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
|
||||
restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
|
||||
token,
|
||||
}),
|
||||
])
|
||||
|
||||
// Format cart
|
||||
const formattedCart = formatCart(cart, Items)
|
||||
// Return cart and errors
|
||||
return {
|
||||
data: formattedCart,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
const headers = {
|
||||
'set-cookie': [
|
||||
serialize(cartCookie, '', {
|
||||
maxAge: -1,
|
||||
path: '/',
|
||||
}),
|
||||
serialize(tokenCookie, '', {
|
||||
maxAge: -1,
|
||||
path: '/',
|
||||
}),
|
||||
],
|
||||
}
|
||||
// Return empty cart
|
||||
res.status(200).json({ data: null, errors: [] })
|
||||
return {
|
||||
data: null,
|
||||
headers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import type { CartSchema } from '@vercel/commerce/types/cart'
|
||||
import type { OrdercloudAPI } from '../..'
|
||||
import type { CartSchema } from '@vercel/commerce/types/cart'
|
||||
|
||||
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||
import cartEndpoint from '@vercel/commerce/api/endpoints/cart'
|
||||
@@ -9,9 +9,8 @@ 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 type CartAPI = GetAPISchema<OrdercloudAPI, CartSchema>
|
||||
|
||||
export const handlers: CartEndpoint['handlers'] = {
|
||||
getCart,
|
||||
|
@@ -1,30 +1,22 @@
|
||||
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]
|
||||
const token = req.cookies.get(tokenCookie)
|
||||
|
||||
// Remove the item to the order
|
||||
await restBuyerFetch(
|
||||
'DELETE',
|
||||
`/orders/Outgoing/${cartId}/lineitems/${itemId}`,
|
||||
null,
|
||||
{ token }
|
||||
{
|
||||
token,
|
||||
}
|
||||
)
|
||||
|
||||
// Get cart
|
||||
@@ -39,7 +31,7 @@ const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
|
||||
const formattedCart = formatCart(cart, lineItems)
|
||||
|
||||
// Return cart and errors
|
||||
res.status(200).json({ data: formattedCart, errors: [] })
|
||||
return { data: formattedCart }
|
||||
}
|
||||
|
||||
export default removeItem
|
||||
|
@@ -6,19 +6,10 @@ 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]
|
||||
const token = req.cookies.get(tokenCookie)
|
||||
|
||||
// Store specs
|
||||
let specs: RawVariant['Specs'] = []
|
||||
@@ -27,9 +18,7 @@ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
||||
if (item.variantId) {
|
||||
specs = await restBuyerFetch(
|
||||
'GET',
|
||||
`/me/products/${item.productId}/variants/${item.variantId}`,
|
||||
null,
|
||||
{ token }
|
||||
`/me/products/${item.productId}/variants/${item.variantId}`
|
||||
).then((res: RawVariant) => res.Specs)
|
||||
}
|
||||
|
||||
@@ -42,7 +31,9 @@ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
||||
Quantity: item.quantity,
|
||||
Specs: specs,
|
||||
},
|
||||
{ token }
|
||||
{
|
||||
token,
|
||||
}
|
||||
)
|
||||
|
||||
// Get cart
|
||||
@@ -57,7 +48,7 @@ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
||||
const formattedCart = formatCart(cart, lineItems)
|
||||
|
||||
// Return cart and errors
|
||||
res.status(200).json({ data: formattedCart, errors: [] })
|
||||
return { data: formattedCart }
|
||||
}
|
||||
|
||||
export default updateItem
|
||||
|
@@ -1,13 +1,14 @@
|
||||
import { normalize as normalizeProduct } from '../../../../utils/product'
|
||||
import { ProductsEndpoint } from '.'
|
||||
import { normalize as normalizeProduct } from '../../../../utils/product'
|
||||
|
||||
// Get products for the product list page. Search and category filter implemented. Sort and brand filter not implemented.
|
||||
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
|
||||
req,
|
||||
res,
|
||||
body: { search, categoryId, brandId, sort },
|
||||
config: { restBuyerFetch, cartCookie, tokenCookie },
|
||||
body: { search, categoryId },
|
||||
config: { restBuyerFetch, tokenCookie },
|
||||
}) => {
|
||||
const token = req.cookies.get(tokenCookie)
|
||||
|
||||
//Use a dummy base as we only care about the relative path
|
||||
const url = new URL('/me/products', 'http://a')
|
||||
|
||||
@@ -18,20 +19,19 @@ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
|
||||
url.searchParams.set('categoryID', String(categoryId))
|
||||
}
|
||||
|
||||
// Get token from cookies
|
||||
const token = req.cookies[tokenCookie]
|
||||
|
||||
var rawProducts = await restBuyerFetch(
|
||||
'GET',
|
||||
url.pathname + url.search,
|
||||
null,
|
||||
{ token }
|
||||
)
|
||||
).then((response: { Items: any[] }) => response.Items)
|
||||
|
||||
const products = rawProducts.Items.map(normalizeProduct)
|
||||
const found = rawProducts?.Items?.length > 0
|
||||
|
||||
res.status(200).json({ data: { products, found } })
|
||||
return {
|
||||
data: {
|
||||
products: rawProducts.map(normalizeProduct),
|
||||
found: rawProducts?.length > 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default getProducts
|
||||
|
@@ -2,27 +2,15 @@ import type { CheckoutEndpoint } from '.'
|
||||
|
||||
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
|
||||
req,
|
||||
res,
|
||||
body: { cartId },
|
||||
config: { restBuyerFetch, tokenCookie },
|
||||
config: { restBuyerFetch },
|
||||
}) => {
|
||||
// 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]
|
||||
const token = req.cookies.get('token')
|
||||
|
||||
// Register credit card
|
||||
const payments = await restBuyerFetch(
|
||||
'GET',
|
||||
`/orders/Outgoing/${cartId}/payments`,
|
||||
null,
|
||||
{ token }
|
||||
`/orders/Outgoing/${cartId}/payments`
|
||||
).then((response: { Items: unknown[] }) => response.Items)
|
||||
|
||||
const address = await restBuyerFetch(
|
||||
@@ -35,15 +23,15 @@ const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
|
||||
)
|
||||
|
||||
// Return cart and errors
|
||||
res.status(200).json({
|
||||
|
||||
return {
|
||||
data: {
|
||||
hasPayment: payments.length > 0,
|
||||
hasShipping: Boolean(address),
|
||||
addressId: address,
|
||||
cardId: payments[0]?.ID,
|
||||
},
|
||||
errors: [],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default getCheckout
|
||||
|
@@ -2,31 +2,18 @@ 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]
|
||||
const token = req.cookies.get(tokenCookie)
|
||||
|
||||
// Submit order
|
||||
await restBuyerFetch(
|
||||
'POST',
|
||||
`/orders/Outgoing/${cartId}/submit`,
|
||||
{},
|
||||
{ token }
|
||||
)
|
||||
await restBuyerFetch('POST', `/orders/Outgoing/${cartId}/submit`, null, {
|
||||
token,
|
||||
})
|
||||
|
||||
// Return cart and errors
|
||||
res.status(200).json({ data: null, errors: [] })
|
||||
return { data: null }
|
||||
}
|
||||
|
||||
export default submitCheckout
|
||||
|
@@ -1,25 +1,11 @@
|
||||
import type { CustomerAddressEndpoint } from '.'
|
||||
|
||||
const addItem: CustomerAddressEndpoint['handlers']['addItem'] = async ({
|
||||
res,
|
||||
req,
|
||||
body: { item, cartId },
|
||||
config: { restBuyerFetch },
|
||||
config: { restBuyerFetch, tokenCookie },
|
||||
}) => {
|
||||
// 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' }],
|
||||
})
|
||||
}
|
||||
const token = req.cookies.get(tokenCookie)
|
||||
|
||||
// Register address
|
||||
const address = await restBuyerFetch('POST', `/me/addresses`, {
|
||||
@@ -37,11 +23,16 @@ const addItem: CustomerAddressEndpoint['handlers']['addItem'] = async ({
|
||||
}).then((response: { ID: string }) => response.ID)
|
||||
|
||||
// Assign address to order
|
||||
await restBuyerFetch('PATCH', `/orders/Outgoing/${cartId}`, {
|
||||
ShippingAddressID: address,
|
||||
})
|
||||
await restBuyerFetch(
|
||||
'PATCH',
|
||||
`/orders/Outgoing/${cartId}`,
|
||||
{
|
||||
ShippingAddressID: address,
|
||||
},
|
||||
{ token }
|
||||
)
|
||||
|
||||
return res.status(200).json({ data: null, errors: [] })
|
||||
return { data: null }
|
||||
}
|
||||
|
||||
export default addItem
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import type { CustomerAddressEndpoint } from '.'
|
||||
|
||||
const getCards: CustomerAddressEndpoint['handlers']['getAddresses'] = async ({
|
||||
res,
|
||||
}) => {
|
||||
return res.status(200).json({ data: null, errors: [] })
|
||||
}
|
||||
const getAddresses: CustomerAddressEndpoint['handlers']['getAddresses'] =
|
||||
() => {
|
||||
return Promise.resolve({ data: null })
|
||||
}
|
||||
|
||||
export default getCards
|
||||
export default getAddresses
|
||||
|
@@ -1,9 +1,7 @@
|
||||
import type { CustomerAddressEndpoint } from '.'
|
||||
|
||||
const removeItem: CustomerAddressEndpoint['handlers']['removeItem'] = async ({
|
||||
res,
|
||||
}) => {
|
||||
return res.status(200).json({ data: null, errors: [] })
|
||||
const removeItem: CustomerAddressEndpoint['handlers']['removeItem'] = () => {
|
||||
return Promise.resolve({ data: null })
|
||||
}
|
||||
|
||||
export default removeItem
|
||||
|
@@ -1,9 +1,7 @@
|
||||
import type { CustomerAddressEndpoint } from '.'
|
||||
|
||||
const updateItem: CustomerAddressEndpoint['handlers']['updateItem'] = async ({
|
||||
res,
|
||||
}) => {
|
||||
return res.status(200).json({ data: null, errors: [] })
|
||||
const updateItem: CustomerAddressEndpoint['handlers']['updateItem'] = () => {
|
||||
return Promise.resolve({ data: null })
|
||||
}
|
||||
|
||||
export default updateItem
|
||||
|
@@ -1,53 +1,47 @@
|
||||
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,
|
||||
req,
|
||||
body: { item, cartId },
|
||||
config: { restBuyerFetch, restMiddlewareFetch },
|
||||
config: { restBuyerFetch, tokenCookie },
|
||||
}) => {
|
||||
// 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({
|
||||
const token = req.cookies.get(tokenCookie)
|
||||
|
||||
const [exp_month, exp_year] = item.cardExpireDate.split('/')
|
||||
const stripeToken = await fetch('https://api.stripe.com/v1/tokens', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.STRIPE_SECRET}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
card: {
|
||||
number: item.cardNumber,
|
||||
exp_month: item.cardExpireDate.split('/')[0],
|
||||
exp_year: item.cardExpireDate.split('/')[1],
|
||||
exp_month,
|
||||
exp_year,
|
||||
cvc: item.cardCvc,
|
||||
},
|
||||
})
|
||||
.then((res: { id: string }) => res.id)
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => 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)
|
||||
const creditCard = await restBuyerFetch(
|
||||
'POST',
|
||||
`/me/creditcards`,
|
||||
{
|
||||
Token: stripeToken,
|
||||
CardType: 'credit',
|
||||
PartialAccountNumber: item.cardNumber.slice(-4),
|
||||
CardholderName: item.cardHolder,
|
||||
ExpirationDate: item.cardExpireDate,
|
||||
},
|
||||
{
|
||||
token,
|
||||
}
|
||||
).then((response: OredercloudCreditCard) => response.ID)
|
||||
|
||||
// Assign payment to order
|
||||
const payment = await restBuyerFetch(
|
||||
@@ -56,19 +50,18 @@ const addItem: CustomerCardEndpoint['handlers']['addItem'] = async ({
|
||||
{
|
||||
Type: 'CreditCard',
|
||||
CreditCardID: creditCard,
|
||||
},
|
||||
{
|
||||
token,
|
||||
}
|
||||
).then((response: { ID: string }) => response.ID)
|
||||
|
||||
// Accept payment to order
|
||||
await restMiddlewareFetch(
|
||||
'PATCH',
|
||||
`/orders/All/${cartId}/payments/${payment}`,
|
||||
{
|
||||
Accepted: true,
|
||||
}
|
||||
)
|
||||
await restBuyerFetch('PATCH', `/orders/All/${cartId}/payments/${payment}`, {
|
||||
Accepted: true,
|
||||
})
|
||||
|
||||
return res.status(200).json({ data: null, errors: [] })
|
||||
return { data: null }
|
||||
}
|
||||
|
||||
export default addItem
|
||||
|
@@ -1,9 +1,7 @@
|
||||
import type { CustomerCardEndpoint } from '.'
|
||||
|
||||
const getCards: CustomerCardEndpoint['handlers']['getCards'] = async ({
|
||||
res,
|
||||
}) => {
|
||||
return res.status(200).json({ data: null, errors: [] })
|
||||
const getCards: CustomerCardEndpoint['handlers']['getCards'] = () => {
|
||||
return Promise.resolve({ data: null })
|
||||
}
|
||||
|
||||
export default getCards
|
||||
|
@@ -1,9 +1,7 @@
|
||||
import type { CustomerCardEndpoint } from '.'
|
||||
|
||||
const removeItem: CustomerCardEndpoint['handlers']['removeItem'] = async ({
|
||||
res,
|
||||
}) => {
|
||||
return res.status(200).json({ data: null, errors: [] })
|
||||
const removeItem: CustomerCardEndpoint['handlers']['removeItem'] = () => {
|
||||
return Promise.resolve({ data: null })
|
||||
}
|
||||
|
||||
export default removeItem
|
||||
|
@@ -1,9 +1,7 @@
|
||||
import type { CustomerCardEndpoint } from '.'
|
||||
|
||||
const updateItem: CustomerCardEndpoint['handlers']['updateItem'] = async ({
|
||||
res,
|
||||
}) => {
|
||||
return res.status(200).json({ data: null, errors: [] })
|
||||
const updateItem: CustomerCardEndpoint['handlers']['updateItem'] = () => {
|
||||
return Promise.resolve({ data: null })
|
||||
}
|
||||
|
||||
export default updateItem
|
||||
|
@@ -1 +0,0 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
21
packages/ordercloud/src/api/endpoints/index.ts
Normal file
21
packages/ordercloud/src/api/endpoints/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { OrdercloudAPI } from '..'
|
||||
|
||||
import createEndpoints from '@vercel/commerce/api/endpoints'
|
||||
|
||||
import cart from './cart'
|
||||
import checkout from './checkout'
|
||||
import products from './catalog/products'
|
||||
import customerCard from './customer/card'
|
||||
import customerAddress from './customer/address'
|
||||
|
||||
const endpoints = {
|
||||
cart,
|
||||
checkout,
|
||||
'customer/card': customerCard,
|
||||
'customer/address': customerAddress,
|
||||
'catalog/products': products,
|
||||
}
|
||||
|
||||
export default function ordercloudAPI(commerce: OrdercloudAPI) {
|
||||
return createEndpoints(commerce, endpoints)
|
||||
}
|
@@ -1 +0,0 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@@ -1 +0,0 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@@ -1 +0,0 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@@ -1 +0,0 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@@ -1,6 +1,6 @@
|
||||
import type { CommerceAPI, CommerceAPIConfig } from '@vercel/commerce/api'
|
||||
import { getCommerceApi as commerceApi } from '@vercel/commerce/api'
|
||||
import { createBuyerFetcher, createMiddlewareFetcher } from './utils/fetch-rest'
|
||||
import { createBuyerFetcher } from './utils/fetch-rest'
|
||||
import createGraphqlFetcher from './utils/fetch-graphql'
|
||||
|
||||
import getAllPages from './operations/get-all-pages'
|
||||
@@ -25,12 +25,6 @@ export interface OrdercloudConfig extends CommerceAPIConfig {
|
||||
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
|
||||
}
|
||||
@@ -44,9 +38,6 @@ const config: OrdercloudConfig = {
|
||||
tokenCookie: TOKEN_COOKIE,
|
||||
cartCookieMaxAge: 2592000,
|
||||
restBuyerFetch: createBuyerFetcher(() => getCommerceApi().getConfig()),
|
||||
restMiddlewareFetch: createMiddlewareFetcher(() =>
|
||||
getCommerceApi().getConfig()
|
||||
),
|
||||
fetch: createGraphqlFetcher(() => getCommerceApi().getConfig()),
|
||||
}
|
||||
|
||||
|
@@ -21,7 +21,7 @@ export function formatCart(
|
||||
name: lineItem.Product.Name,
|
||||
quantity: lineItem.Quantity,
|
||||
discounts: [],
|
||||
path: lineItem.ProductID,
|
||||
path: `/product/${lineItem.ProductID}`,
|
||||
variant: {
|
||||
id: lineItem.Variant ? String(lineItem.Variant.ID) : '',
|
||||
sku: lineItem.ID,
|
||||
|
@@ -1,11 +1,7 @@
|
||||
import vercelFetch from '@vercel/fetch'
|
||||
import { FetcherError } from '@vercel/commerce/utils/errors'
|
||||
import { CustomNodeJsGlobal } from '../../types/node';
|
||||
|
||||
import { OrdercloudConfig } from '../index'
|
||||
|
||||
// Get an instance to vercel fetch
|
||||
const fetch = vercelFetch()
|
||||
export let token: string | null = null
|
||||
|
||||
// Get token util
|
||||
async function getToken({
|
||||
@@ -16,7 +12,12 @@ async function getToken({
|
||||
baseUrl: string
|
||||
clientId: string
|
||||
clientSecret?: string
|
||||
}): Promise<string> {
|
||||
}): Promise<{
|
||||
access_token: string
|
||||
expires_in: number
|
||||
refresh_token: string
|
||||
token_type: string
|
||||
}> {
|
||||
// If not, get a new one and store it
|
||||
const authResponse = await fetch(`${baseUrl}/oauth/token`, {
|
||||
method: 'POST',
|
||||
@@ -32,6 +33,8 @@ async function getToken({
|
||||
// Get the body of it
|
||||
const error = await authResponse.json()
|
||||
|
||||
console.log(JSON.stringify(error, null, 2))
|
||||
|
||||
// And return an error
|
||||
throw new FetcherError({
|
||||
errors: [{ message: error.error_description.Code }],
|
||||
@@ -40,9 +43,7 @@ async function getToken({
|
||||
}
|
||||
|
||||
// Return the token
|
||||
return authResponse
|
||||
.json()
|
||||
.then((response: { access_token: string }) => response.access_token)
|
||||
return authResponse.json()
|
||||
}
|
||||
|
||||
export async function fetchData<T>(opts: {
|
||||
@@ -74,12 +75,18 @@ export async function fetchData<T>(opts: {
|
||||
|
||||
// If something failed getting the data response
|
||||
if (!dataResponse.ok) {
|
||||
// Get the body of it
|
||||
const error = await dataResponse.textConverted()
|
||||
let errors
|
||||
|
||||
// And return an error
|
||||
try {
|
||||
// Get the body of it
|
||||
const error = await dataResponse.json()
|
||||
errors = error.Errors
|
||||
} catch (e) {
|
||||
const message = await dataResponse.text()
|
||||
errors = [{ message }]
|
||||
}
|
||||
throw new FetcherError({
|
||||
errors: [{ message: error || dataResponse.statusText }],
|
||||
errors,
|
||||
status: dataResponse.status,
|
||||
})
|
||||
}
|
||||
@@ -93,42 +100,6 @@ export async function fetchData<T>(opts: {
|
||||
}
|
||||
}
|
||||
|
||||
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>(
|
||||
@@ -144,28 +115,27 @@ export const createBuyerFetcher: (
|
||||
body?: Record<string, unknown>,
|
||||
fetchOptions?: Record<string, any>
|
||||
) => {
|
||||
const customGlobal = global as unknown as CustomNodeJsGlobal;
|
||||
if (fetchOptions?.token) {
|
||||
token = fetchOptions?.token
|
||||
}
|
||||
|
||||
// Get provider config
|
||||
const config = getConfig()
|
||||
|
||||
let meta: any = {}
|
||||
|
||||
// If a token was passed, set it on global
|
||||
if (fetchOptions?.token) {
|
||||
customGlobal.token = fetchOptions.token
|
||||
}
|
||||
|
||||
// Get a token
|
||||
if (!customGlobal.token) {
|
||||
customGlobal.token = await getToken({
|
||||
if (!token) {
|
||||
const newToken = await getToken({
|
||||
baseUrl: config.commerceUrl,
|
||||
clientId: process.env.ORDERCLOUD_BUYER_CLIENT_ID as string,
|
||||
})
|
||||
token = newToken.access_token
|
||||
meta.token = newToken
|
||||
}
|
||||
|
||||
// Return the data and specify the expected type
|
||||
const data = await fetchData<T>({
|
||||
token: customGlobal.token as string,
|
||||
token,
|
||||
fetchOptions,
|
||||
config,
|
||||
method,
|
||||
@@ -175,6 +145,6 @@ export const createBuyerFetcher: (
|
||||
|
||||
return {
|
||||
...data,
|
||||
meta: { token: customGlobal.token as string },
|
||||
meta,
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<AddItemHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
url: '/api/commerce/cart',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: item, options, fetch }) {
|
||||
|
@@ -8,7 +8,7 @@ export default useCart as UseCart<typeof handler>
|
||||
|
||||
export const handler: SWRHook<GetCartHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
url: '/api/commerce/cart',
|
||||
method: 'GET',
|
||||
},
|
||||
useHook: ({ useData }) =>
|
||||
|
@@ -2,12 +2,18 @@ import type {
|
||||
MutationHookContext,
|
||||
HookFetcherContext,
|
||||
} from '@vercel/commerce/utils/types'
|
||||
import type { Cart, LineItem, RemoveItemHook } from '@vercel/commerce/types/cart'
|
||||
import type {
|
||||
Cart,
|
||||
LineItem,
|
||||
RemoveItemHook,
|
||||
} from '@vercel/commerce/types/cart'
|
||||
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { ValidationError } from '@vercel/commerce/utils/errors'
|
||||
import useRemoveItem, { UseRemoveItem } from '@vercel/commerce/cart/use-remove-item'
|
||||
import useRemoveItem, {
|
||||
UseRemoveItem,
|
||||
} from '@vercel/commerce/cart/use-remove-item'
|
||||
|
||||
import useCart from './use-cart'
|
||||
|
||||
@@ -23,7 +29,7 @@ export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
url: '/api/commerce/cart',
|
||||
method: 'DELETE',
|
||||
},
|
||||
async fetcher({
|
||||
|
@@ -9,7 +9,9 @@ import debounce from 'lodash.debounce'
|
||||
|
||||
import { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import { ValidationError } from '@vercel/commerce/utils/errors'
|
||||
import useUpdateItem, { UseUpdateItem } from '@vercel/commerce/cart/use-update-item'
|
||||
import useUpdateItem, {
|
||||
UseUpdateItem,
|
||||
} from '@vercel/commerce/cart/use-update-item'
|
||||
|
||||
import { handler as removeItemHandler } from './use-remove-item'
|
||||
import useCart from './use-cart'
|
||||
@@ -22,7 +24,7 @@ export default useUpdateItem as UseUpdateItem<any>
|
||||
|
||||
export const handler: MutationHook<any> = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
url: '/api/commerce/cart',
|
||||
method: 'PUT',
|
||||
},
|
||||
async fetcher({
|
||||
|
@@ -11,7 +11,7 @@ export default useCheckout as UseCheckout<typeof handler>
|
||||
|
||||
export const handler: SWRHook<GetCheckoutHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/checkout',
|
||||
url: '/api/commerce/checkout',
|
||||
method: 'GET',
|
||||
},
|
||||
useHook: ({ useData }) =>
|
||||
|
@@ -10,7 +10,7 @@ export default useSubmitCheckout as UseSubmitCheckout<typeof handler>
|
||||
|
||||
export const handler: MutationHook<SubmitCheckoutHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/checkout',
|
||||
url: '/api/commerce/checkout',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: item, options, fetch }) {
|
||||
|
@@ -2,14 +2,16 @@ import type { AddItemHook } from '@vercel/commerce/types/customer/address'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import useAddItem, { UseAddItem } from '@vercel/commerce/customer/address/use-add-item'
|
||||
import useAddItem, {
|
||||
UseAddItem,
|
||||
} from '@vercel/commerce/customer/address/use-add-item'
|
||||
import useAddresses from './use-addresses'
|
||||
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<AddItemHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/customer/address',
|
||||
url: '/api/commerce/customer/address',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: item, options, fetch }) {
|
||||
@@ -27,9 +29,7 @@ export const handler: MutationHook<AddItemHook> = {
|
||||
return useCallback(
|
||||
async function addItem(input) {
|
||||
const data = await fetch({ input })
|
||||
|
||||
await mutate([data], false)
|
||||
|
||||
await mutate(data ? [data] : [], false)
|
||||
return data
|
||||
},
|
||||
[fetch, mutate]
|
||||
|
@@ -10,7 +10,7 @@ export default useAddresses as UseAddresses<typeof handler>
|
||||
|
||||
export const handler: SWRHook<GetAddressesHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/customer/address',
|
||||
url: '/api/commerce/customer/address',
|
||||
method: 'GET',
|
||||
},
|
||||
useHook: ({ useData }) =>
|
||||
|
@@ -2,7 +2,10 @@ import type {
|
||||
MutationHookContext,
|
||||
HookFetcherContext,
|
||||
} from '@vercel/commerce/utils/types'
|
||||
import type { Address, RemoveItemHook } from '@vercel/commerce/types/customer/address'
|
||||
import type {
|
||||
Address,
|
||||
RemoveItemHook,
|
||||
} from '@vercel/commerce/types/customer/address'
|
||||
|
||||
import { useCallback } from 'react'
|
||||
|
||||
@@ -14,7 +17,7 @@ import useRemoveItem, {
|
||||
import useAddresses from './use-addresses'
|
||||
|
||||
export type RemoveItemFn<T = any> = T extends Address
|
||||
? (input?: RemoveItemActionInput<T>) => Promise<Address | null | undefined>
|
||||
? (input?: RemoveItemActionInput<T>) => Promise<Address | null>
|
||||
: (input: RemoveItemActionInput<T>) => Promise<Address | null>
|
||||
|
||||
export type RemoveItemActionInput<T = any> = T extends Address
|
||||
@@ -25,7 +28,7 @@ export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
url: '/api/customer/address',
|
||||
url: '/api/commerce/customer/address',
|
||||
method: 'DELETE',
|
||||
},
|
||||
async fetcher({
|
||||
|
@@ -2,7 +2,10 @@ import type {
|
||||
HookFetcherContext,
|
||||
MutationHookContext,
|
||||
} from '@vercel/commerce/utils/types'
|
||||
import type { UpdateItemHook, Address } from '@vercel/commerce/types/customer/address'
|
||||
import type {
|
||||
UpdateItemHook,
|
||||
Address,
|
||||
} from '@vercel/commerce/types/customer/address'
|
||||
|
||||
import { useCallback } from 'react'
|
||||
|
||||
@@ -21,7 +24,7 @@ export default useUpdateItem as UseUpdateItem<any>
|
||||
|
||||
export const handler: MutationHook<any> = {
|
||||
fetchOptions: {
|
||||
url: '/api/customer/address',
|
||||
url: '/api/commerce/customer/address',
|
||||
method: 'PUT',
|
||||
},
|
||||
async fetcher({
|
||||
|
@@ -2,14 +2,16 @@ import type { AddItemHook } from '@vercel/commerce/types/customer/card'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import useAddItem, { UseAddItem } from '@vercel/commerce/customer/card/use-add-item'
|
||||
import useAddItem, {
|
||||
UseAddItem,
|
||||
} from '@vercel/commerce/customer/card/use-add-item'
|
||||
import useCards from './use-cards'
|
||||
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<AddItemHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/customer/card',
|
||||
url: '/api/commerce/customer/card',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: item, options, fetch }) {
|
||||
@@ -28,7 +30,7 @@ export const handler: MutationHook<AddItemHook> = {
|
||||
async function addItem(input) {
|
||||
const data = await fetch({ input })
|
||||
|
||||
await mutate([data], false)
|
||||
await mutate(data ? [data] : [], false)
|
||||
|
||||
return data
|
||||
},
|
||||
|
@@ -8,7 +8,7 @@ export default useCard as UseCards<typeof handler>
|
||||
|
||||
export const handler: SWRHook<GetCardsHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/customer/card',
|
||||
url: '/api/commerce/customer/card',
|
||||
method: 'GET',
|
||||
},
|
||||
useHook: ({ useData }) =>
|
||||
|
@@ -14,7 +14,7 @@ import useRemoveItem, {
|
||||
import useCards from './use-cards'
|
||||
|
||||
export type RemoveItemFn<T = any> = T extends Card
|
||||
? (input?: RemoveItemActionInput<T>) => Promise<Card | null | undefined>
|
||||
? (input?: RemoveItemActionInput<T>) => Promise<Card | null>
|
||||
: (input: RemoveItemActionInput<T>) => Promise<Card | null>
|
||||
|
||||
export type RemoveItemActionInput<T = any> = T extends Card
|
||||
@@ -25,7 +25,7 @@ export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
url: '/api/customer/card',
|
||||
url: '/api/commerce/customer/card',
|
||||
method: 'DELETE',
|
||||
},
|
||||
async fetcher({
|
||||
|
@@ -21,7 +21,7 @@ export default useUpdateItem as UseUpdateItem<any>
|
||||
|
||||
export const handler: MutationHook<any> = {
|
||||
fetchOptions: {
|
||||
url: '/api/customer/card',
|
||||
url: '/api/commerce/customer/card',
|
||||
method: 'PUT',
|
||||
},
|
||||
async fetcher({
|
||||
|
@@ -5,7 +5,7 @@ export default useSearch as UseSearch<typeof handler>
|
||||
|
||||
export const handler: SWRHook<SearchProductsHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/catalog/products',
|
||||
url: '/api/commerce/catalog/products',
|
||||
method: 'GET',
|
||||
},
|
||||
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
interface RawVariantSpec {
|
||||
export interface RawVariantSpec {
|
||||
SpecID: string
|
||||
Name: string
|
||||
OptionID: string
|
||||
|
Reference in New Issue
Block a user