Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/Cart and order data persistence on the server side #3

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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