Skip to content

Commit

Permalink
Cart and order data persistence on the server side
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcin Slezak committed Oct 30, 2023
1 parent 0404be1 commit c50197b
Show file tree
Hide file tree
Showing 17 changed files with 269 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export const CheckoutProvider = ({ children }: CheckoutProviderProps) => {
*/

const [checkoutState, setCheckoutState] = useState<CheckoutState>({
cartId: cart.id || '',
config: {
billingSameAsShipping: true,
gdpr: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { InputField } from '@composable/ui'
import { CHECKOUT_FORM_KEY } from '../constants'
import { CheckoutInput } from '@composable/types'

type FormData = Omit<CheckoutInput['customer'], 'id' | 'name'>
type FormData = Omit<CheckoutInput['customer'], 'id' | 'name' | 'cartId'>

type GuestFormProps = Pick<
UseCheckoutFormProps<FormData>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const createOrder = protectedProcedure
.input(
z.object({
checkout: z.object({
cartId: z.string(),
customer: z.object({
id: z.string().optional(),
email: z.string(),
Expand Down
6 changes: 6 additions & 0 deletions composable-ui/src/utils/__mocks__/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,27 @@ export const api = {
items: [
{
__typename: 'BannerSplit',
id: '23385feb-cd12-4122-8105-bf5da178d70c',
},
{
__typename: 'BannerFull',
id: '2d095fdd-e3ea-4f7a-907a-359ef1d0593d',
},
{
__typename: 'BannerTextOnly',
id: '40433348-1eb0-43fd-99dc-090c79972512',
},
{
__typename: 'BannerTextOnly',
id: 'a609a45e-f3f2-4cfc-8709-29a08153c9ac',
},
{
__typename: 'Grid',
id: '9fad8aa3-6e8d-43e2-b79a-492409b49003',
},
{
__typename: 'CommerceConnector',
id: '662f1110-8a51-4a11-9a8e-730a43d6867d',
},
],
},
Expand Down
4 changes: 3 additions & 1 deletion packages/commerce-generic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"ts": "tsc --noEmit --incremental"
},
"dependencies": {
"@composable/types": "workspace:*"
"@composable/types": "workspace:*",
"@types/node-persist": "^3.1.5",
"node-persist": "^3.1.3"
},
"devDependencies": {
"eslint-config-custom": "workspace:*",
Expand Down
44 changes: 44 additions & 0 deletions packages/commerce-generic/src/data/generate-cart-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Cart, CartItem } from '@composable/types'
import products from './products.json'
import { randomUUID } from 'crypto'

const findProductById = (id: string) => {
return products.find((product) => product.id === id) ?? products[0]
}

export const generateEmptyCart = (cartId?: string): Cart => ({
id: cartId || randomUUID(),
items: [],
summary: {},
})

export const generateCartItem = (productId: string, quantity: number) => {
const _product = findProductById(productId)
return {
brand: _product.brand,
category: _product.category,
id: _product.id,
image: _product.images[0],
name: _product.name,
price: _product.price,
quantity: quantity ?? 1,
sku: _product.sku,
slug: _product.slug,
type: _product.type,
}
}

export const calculateCartSummary = (cartItems: CartItem[]) => {
const subtotal = cartItems.reduce((_subtotal, item) => {
return _subtotal + item.price * (item.quantity ?? 1)
}, 0)
const taxes = subtotal * 0.07
const total = subtotal + taxes

return {
subtotalPrice: subtotal.toFixed(2),
taxes: taxes.toFixed(2),
totalPrice: total.toFixed(2),
shipping: 'Free',
}
}
38 changes: 0 additions & 38 deletions packages/commerce-generic/src/data/generateCartData.ts

This file was deleted.

33 changes: 33 additions & 0 deletions packages/commerce-generic/src/data/persit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import storage from 'node-persist'
import path from 'path'
import os from 'os'
import { Order, Cart } from '@composable/types'

const storageFolderPath = path.join(os.tmpdir(), 'composable-ui-storage')

storage.init({
dir: storageFolderPath,
})

export const getOrder = async (orderId: string): Promise<Order | undefined> => {
return storage.getItem(`order-${orderId}`)
}

export const saveOrder = async (order: Order) => {
await storage.setItem(`order-${order.id}`, order)
return order
}

export const getCart = async (cartId: string): Promise<Cart | undefined> => {
return storage.getItem(`cart-${cartId}`)
}

export const saveCart = async (cart: Cart) => {
await storage.setItem(`cart-${cart.id}`, cart)
return cart
}

export const deleteCart = async (cartId: string) => {
const result = await storage.del(`cart-${cartId}`)
return result.removed
}
26 changes: 19 additions & 7 deletions packages/commerce-generic/src/services/cart/add-cart-item.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { CommerceService } from '@composable/types'
import { generateCartData } from '../../data/generateCartData'
import cart from '../../data/cart.json'
import { getCart, saveCart } from '../../data/persit'
import {
generateCartItem,
calculateCartSummary,
generateEmptyCart,
} from '../../data/generate-cart-data'

export const addCartItem: CommerceService['addCartItem'] = async ({
cartId,
productId,
quantity,
variantId,
}) => {
const { items, summary } = generateCartData({ productId, quantity })
const cart = (await getCart(cartId)) || generateEmptyCart(cartId)

return {
...cart,
items,
summary,
const isProductInCartAlready = cart.items.some(
(item) => item.id === productId
)

if (isProductInCartAlready) {
cart.items.find((item) => item.id === productId)!.quantity++
} else {
const newItem = generateCartItem(productId, quantity)
cart.items.push(newItem)
}
cart.summary = calculateCartSummary(cart.items)

return saveCart(cart)
}
5 changes: 3 additions & 2 deletions packages/commerce-generic/src/services/cart/create-cart.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CommerceService } from '@composable/types'
import cart from '../../data/cart.json'
import { saveCart } from '../../data/persit'
import { generateEmptyCart } from '../../data/generate-cart-data'

export const createCart: CommerceService['createCart'] = async () => {
return cart
return saveCart(generateEmptyCart())
}
17 changes: 15 additions & 2 deletions packages/commerce-generic/src/services/cart/delete-cart-item.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import { CommerceService } from '@composable/types'
import cart from '../../data/cart.json'
import { getCart, saveCart } from '../../data/persit'

import { calculateCartSummary } from '../../data/generate-cart-data'

export const deleteCartItem: CommerceService['deleteCartItem'] = async ({
cartId,
productId,
}) => {
return cart
const cart = await getCart(cartId)

if (!cart) {
throw new Error(
`[deleteCartItem] Could not found cart with requested cart id: ${cartId}`
)
}

cart.items = cart.items.filter((item) => item.id !== productId)
cart.summary = calculateCartSummary(cart.items)

return saveCart(cart)
}
11 changes: 2 additions & 9 deletions packages/commerce-generic/src/services/cart/get-cart.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import { CommerceService } from '@composable/types'
import { generateCartData } from '../../data/generateCartData'
import cart from '../../data/cart.json'
import { getCart as getCartFromStorage } from '../../data/persit'

export const getCart: CommerceService['getCart'] = async ({ cartId }) => {
if (!cartId) {
return null
}

const { items, summary } = generateCartData()

return {
...cart,
items,
summary,
}
return (await getCartFromStorage(cartId)) || null
}
29 changes: 22 additions & 7 deletions packages/commerce-generic/src/services/cart/update-cart-item.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import { CommerceService } from '@composable/types'
import { generateCartData } from '../../data/generateCartData'
import cart from '../../data/cart.json'
import { getCart, saveCart } from '../../data/persit'

import { calculateCartSummary } from '../../data/generate-cart-data'

export const updateCartItem: CommerceService['updateCartItem'] = async ({
cartId,
productId,
quantity,
}) => {
const { items, summary } = generateCartData({ productId, quantity })
const cart = await getCart(cartId)

if (!cart) {
throw new Error(
`[updateCartItem] Could not found cart with requested cart id: ${cartId}`
)
}

const cartItem = cart.items.find((item) => item.id === productId)

return {
...cart,
items,
summary,
if (!cartItem) {
throw new Error(
`[updateCartItem] Could not found cart item with requested product id: ${productId}`
)
}

cartItem.quantity = quantity

cart.summary = calculateCartSummary(cart.items)

return saveCart(cart)
}
50 changes: 41 additions & 9 deletions packages/commerce-generic/src/services/checkout/create-order.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,48 @@
import { CommerceService } from '@composable/types'
import { generateCartData } from '../../data/generateCartData'
import order from '../../data/order.json'
import { Cart, CheckoutInput, CommerceService, Order } from '@composable/types'
import { getCart } from '../../data/persit'
import { saveOrder } from '../../data/persit'
import shippingMethods from '../../data/shipping-methods.json'
import { randomUUID } from 'crypto'

export const createOrder: CommerceService['createOrder'] = async ({
checkout,
}) => {
const cartItems = generateCartData()
const generateOrderFromCart = (
cart: Cart,
checkoutInput: CheckoutInput
): Order => {
return {
...order,
...cartItems,
id: randomUUID(),
status: 'complete',
payment: 'unpaid',
shipping: 'unfulfilled',
customer: {
email: checkoutInput.customer.email,
},
shipping_address: {
phone_number: '',
city: '',
...checkoutInput.shipping_address,
},
billing_address: {
phone_number: '',
city: '',
...checkoutInput.billing_address,
},
shipping_method: shippingMethods[0],
created_at: Date.now(),
items: cart.items,
summary: cart.summary,
}
}

export const createOrder: CommerceService['createOrder'] = async ({
checkout,
}) => {
const cart = await getCart(checkout.cartId)

if (!cart) {
throw new Error(
`[createOrder] Could not find cart by id: ${checkout.cartId}`
)
}

return saveOrder(generateOrderFromCart(cart, checkout))
}
10 changes: 7 additions & 3 deletions packages/commerce-generic/src/services/checkout/get-order.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { CommerceService } from '@composable/types'
import { generateCartData } from '../../data/generateCartData'
import { getOrder as getOrerFromStorage } from '../../data/persit'
import order from '../../data/order.json'
import shippingMethods from '../../data/shipping-methods.json'

export const getOrder: CommerceService['getOrder'] = async ({ orderId }) => {
const cartItems = generateCartData()
const order = await getOrerFromStorage(orderId)

if (!order) {
throw new Error(`[getOrder] Could not found order: ${orderId}`)
}

return {
...order,
...cartItems,
shipping_method: shippingMethods[0],
created_at: Date.now(),
}
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/commerce/checkout.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface CheckoutInput {
cartId: string
customer: {
id?: string
email: string
Expand Down
Loading

0 comments on commit c50197b

Please sign in to comment.