2026-07-09 17:55:07 +00:00
|
|
|
import { BillingCycle, BasketItem, ProductDto } from '@/proxy/order/models'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
export interface CartState {
|
2026-07-09 17:55:07 +00:00
|
|
|
items: BasketItem[]
|
|
|
|
|
subtotal: number
|
|
|
|
|
vatTotal: number
|
|
|
|
|
total: number
|
|
|
|
|
globalBillingCycle: BillingCycle
|
|
|
|
|
globalPeriod: number
|
2026-02-24 20:44:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const initialCartState: CartState = {
|
|
|
|
|
items: [],
|
2026-07-07 13:41:07 +00:00
|
|
|
subtotal: 0,
|
|
|
|
|
vatTotal: 0,
|
2026-02-24 20:44:16 +00:00
|
|
|
total: 0,
|
|
|
|
|
globalBillingCycle: 'yearly',
|
2026-07-09 17:55:07 +00:00
|
|
|
globalPeriod: 1,
|
|
|
|
|
}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
export const calculateItemPrice = (
|
|
|
|
|
product: ProductDto,
|
|
|
|
|
quantity: number,
|
|
|
|
|
billingCycle: BillingCycle,
|
2026-07-09 17:55:07 +00:00
|
|
|
period: number = 1,
|
2026-02-24 20:44:16 +00:00
|
|
|
): number => {
|
2026-07-09 17:55:07 +00:00
|
|
|
const price = billingCycle === 'monthly' ? product.monthlyPrice! : product.yearlyPrice!
|
|
|
|
|
const vatRate = product.vatRate ?? 0.2
|
|
|
|
|
return roundMoney(price * quantity * period * (1 + vatRate))
|
|
|
|
|
}
|
2026-07-07 13:41:07 +00:00
|
|
|
|
2026-07-09 17:55:07 +00:00
|
|
|
const roundMoney = (value: number): number => Math.round(value * 100) / 100
|
2026-07-07 13:41:07 +00:00
|
|
|
|
2026-07-09 17:55:07 +00:00
|
|
|
const calculateItemVatAmount = (totalPrice: number, vatRate: number = 0.2): number =>
|
|
|
|
|
vatRate > 0 ? roundMoney(totalPrice - totalPrice / (1 + vatRate)) : 0
|
2026-07-07 13:41:07 +00:00
|
|
|
|
|
|
|
|
const withItemVat = (
|
|
|
|
|
product: ProductDto,
|
|
|
|
|
quantity: number,
|
|
|
|
|
billingCycle: BillingCycle,
|
|
|
|
|
period: number,
|
|
|
|
|
): BasketItem => {
|
2026-07-09 17:55:07 +00:00
|
|
|
const vatRate = product.vatRate ?? 0.2
|
|
|
|
|
const totalPrice = calculateItemPrice(product, quantity, billingCycle, period)
|
2026-07-07 13:41:07 +00:00
|
|
|
return {
|
|
|
|
|
product,
|
|
|
|
|
quantity,
|
|
|
|
|
billingCycle,
|
|
|
|
|
vatRate,
|
|
|
|
|
vatAmount: calculateItemVatAmount(totalPrice, vatRate),
|
|
|
|
|
totalPrice,
|
2026-07-09 17:55:07 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-07-07 13:41:07 +00:00
|
|
|
|
|
|
|
|
const calculateCartTotals = (items: BasketItem[]) => {
|
2026-07-09 17:55:07 +00:00
|
|
|
const total = roundMoney(items.reduce((sum, item) => sum + item.totalPrice, 0))
|
|
|
|
|
const vatTotal = roundMoney(items.reduce((sum, item) => sum + (item.vatAmount ?? 0), 0))
|
2026-07-07 13:41:07 +00:00
|
|
|
return {
|
|
|
|
|
subtotal: roundMoney(total - vatTotal),
|
|
|
|
|
vatTotal,
|
|
|
|
|
total,
|
2026-07-09 17:55:07 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
export const addItemToCart = (
|
|
|
|
|
cartState: CartState,
|
|
|
|
|
product: ProductDto,
|
|
|
|
|
quantity: number,
|
2026-07-09 17:55:07 +00:00
|
|
|
billingCycle: BillingCycle,
|
2026-02-24 20:44:16 +00:00
|
|
|
): CartState => {
|
2026-07-09 17:55:07 +00:00
|
|
|
const isAlreadyInCart = isItemInCartState(cartState.items, product.id, billingCycle)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
if (!product.isQuantityBased && isAlreadyInCart) {
|
2026-07-09 17:55:07 +00:00
|
|
|
return cartState
|
2026-02-24 20:44:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const existingItemIndex = cartState.items.findIndex(
|
2026-07-09 17:55:07 +00:00
|
|
|
(item) => item.product.id === product.id && item.billingCycle === billingCycle,
|
|
|
|
|
)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
2026-07-09 17:55:07 +00:00
|
|
|
let newItems
|
2026-02-24 20:44:16 +00:00
|
|
|
if (existingItemIndex > -1) {
|
2026-07-09 17:55:07 +00:00
|
|
|
newItems = [...cartState.items]
|
|
|
|
|
newItems[existingItemIndex].quantity += quantity
|
2026-07-07 13:41:07 +00:00
|
|
|
newItems[existingItemIndex] = withItemVat(
|
2026-02-24 20:44:16 +00:00
|
|
|
product,
|
|
|
|
|
newItems[existingItemIndex].quantity,
|
|
|
|
|
billingCycle,
|
2026-07-07 13:41:07 +00:00
|
|
|
cartState.globalPeriod,
|
2026-07-09 17:55:07 +00:00
|
|
|
)
|
2026-02-24 20:44:16 +00:00
|
|
|
} else {
|
2026-07-09 17:55:07 +00:00
|
|
|
const actualQuantity = !product.isQuantityBased ? 1 : quantity
|
2026-07-07 13:41:07 +00:00
|
|
|
newItems = [
|
|
|
|
|
...cartState.items,
|
|
|
|
|
withItemVat(product, actualQuantity, billingCycle, cartState.globalPeriod),
|
2026-07-09 17:55:07 +00:00
|
|
|
]
|
2026-02-24 20:44:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-09 17:55:07 +00:00
|
|
|
const updatedState = { ...cartState, items: newItems, ...calculateCartTotals(newItems) }
|
|
|
|
|
return updatedState
|
|
|
|
|
}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
export const removeItemFromCart = (cartState: CartState, id: string): CartState => {
|
2026-07-09 17:55:07 +00:00
|
|
|
const newItems = cartState.items.filter((item) => item.product.id !== id)
|
|
|
|
|
const updatedState = { ...cartState, items: newItems, ...calculateCartTotals(newItems) }
|
|
|
|
|
return updatedState
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const updateCartItemQuantity = (
|
|
|
|
|
cartState: CartState,
|
|
|
|
|
id: string,
|
|
|
|
|
quantity: number,
|
|
|
|
|
): CartState => {
|
|
|
|
|
const newItems = cartState.items
|
|
|
|
|
.map((item) => {
|
|
|
|
|
if (item.product.id === id) {
|
|
|
|
|
if (quantity <= 0) return null
|
|
|
|
|
if (!item.product.isQuantityBased) return item
|
|
|
|
|
|
|
|
|
|
return withItemVat(item.product, quantity, item.billingCycle, cartState.globalPeriod)
|
|
|
|
|
}
|
|
|
|
|
return item
|
|
|
|
|
})
|
|
|
|
|
.filter(Boolean) as BasketItem[]
|
|
|
|
|
|
|
|
|
|
const updatedState = { ...cartState, items: newItems, ...calculateCartTotals(newItems) }
|
|
|
|
|
return updatedState
|
|
|
|
|
}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
export const clearCart = (): CartState => {
|
2026-07-09 17:55:07 +00:00
|
|
|
return initialCartState
|
|
|
|
|
}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
export const setCartGlobalBillingCycle = (cartState: CartState, cycle: BillingCycle): CartState => {
|
2026-07-09 17:55:07 +00:00
|
|
|
const newItems = cartState.items.map((item) =>
|
|
|
|
|
withItemVat(item.product, item.quantity, cycle, cartState.globalPeriod),
|
|
|
|
|
)
|
|
|
|
|
const updatedState = {
|
|
|
|
|
...cartState,
|
|
|
|
|
globalBillingCycle: cycle,
|
|
|
|
|
items: newItems,
|
|
|
|
|
...calculateCartTotals(newItems),
|
|
|
|
|
}
|
|
|
|
|
return updatedState
|
|
|
|
|
}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
export const setCartGlobalPeriod = (cartState: CartState, period: number): CartState => {
|
2026-07-09 17:55:07 +00:00
|
|
|
const newItems = cartState.items.map((item) =>
|
|
|
|
|
withItemVat(item.product, item.quantity, item.billingCycle, period),
|
|
|
|
|
)
|
|
|
|
|
const updatedState = {
|
|
|
|
|
...cartState,
|
|
|
|
|
globalPeriod: period,
|
|
|
|
|
items: newItems,
|
|
|
|
|
...calculateCartTotals(newItems),
|
|
|
|
|
}
|
|
|
|
|
return updatedState
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const isItemInCart = (
|
|
|
|
|
cartState: CartState,
|
|
|
|
|
productId: string,
|
|
|
|
|
billingCycle: BillingCycle,
|
|
|
|
|
): boolean => {
|
2026-02-24 20:44:16 +00:00
|
|
|
return cartState.items.some(
|
2026-07-09 17:55:07 +00:00
|
|
|
(item) => item.product.id === productId && item.billingCycle === billingCycle,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const isItemInCartState = (
|
|
|
|
|
items: BasketItem[],
|
|
|
|
|
productId: string,
|
|
|
|
|
billingCycle: BillingCycle,
|
|
|
|
|
): boolean => {
|
|
|
|
|
return items.some((item) => item.product.id === productId && item.billingCycle === billingCycle)
|
|
|
|
|
}
|