176 lines
4.9 KiB
TypeScript
176 lines
4.9 KiB
TypeScript
import { BillingCycle, BasketItem, ProductDto } from '@/proxy/order/models'
|
|
|
|
export interface BasketData {
|
|
items: BasketItem[]
|
|
subtotal: number
|
|
vatTotal: number
|
|
total: number
|
|
globalBillingCycle: BillingCycle
|
|
globalPeriod: number
|
|
}
|
|
|
|
export const initialBasketData: BasketData = {
|
|
items: [],
|
|
subtotal: 0,
|
|
vatTotal: 0,
|
|
total: 0,
|
|
globalBillingCycle: 'yearly',
|
|
globalPeriod: 1,
|
|
}
|
|
|
|
export const calculateItemPrice = (
|
|
product: ProductDto,
|
|
quantity: number,
|
|
billingCycle: BillingCycle,
|
|
period: number = 1,
|
|
): number => {
|
|
const price = billingCycle === 'monthly' ? product.monthlyPrice! : product.yearlyPrice!
|
|
const vatRate = product.vatRate ?? 0.2
|
|
return roundMoney(price * quantity * period * (1 + vatRate))
|
|
}
|
|
|
|
const roundMoney = (value: number): number => Math.round(value * 100) / 100
|
|
|
|
const calculateItemVatAmount = (totalPrice: number, vatRate: number = 0.2): number =>
|
|
vatRate > 0 ? roundMoney(totalPrice - totalPrice / (1 + vatRate)) : 0
|
|
|
|
const withItemVat = (
|
|
product: ProductDto,
|
|
quantity: number,
|
|
billingCycle: BillingCycle,
|
|
period: number,
|
|
): BasketItem => {
|
|
const vatRate = product.vatRate ?? 0.2
|
|
const totalPrice = calculateItemPrice(product, quantity, billingCycle, period)
|
|
return {
|
|
product,
|
|
quantity,
|
|
billingCycle,
|
|
vatRate,
|
|
vatAmount: calculateItemVatAmount(totalPrice, vatRate),
|
|
totalPrice,
|
|
}
|
|
}
|
|
|
|
const calculateBasketTotals = (items: BasketItem[]) => {
|
|
const total = roundMoney(items.reduce((sum, item) => sum + item.totalPrice, 0))
|
|
const vatTotal = roundMoney(items.reduce((sum, item) => sum + (item.vatAmount ?? 0), 0))
|
|
return {
|
|
subtotal: roundMoney(total - vatTotal),
|
|
vatTotal,
|
|
total,
|
|
}
|
|
}
|
|
|
|
export const addItemToBasket = (
|
|
basketData: BasketData,
|
|
product: ProductDto,
|
|
quantity: number,
|
|
billingCycle: BillingCycle,
|
|
): BasketData => {
|
|
const isAlreadyInBasket = isItemInBasketData(basketData.items, product.id, billingCycle)
|
|
|
|
if (!product.isQuantityBased && isAlreadyInBasket) {
|
|
return basketData
|
|
}
|
|
|
|
const existingItemIndex = basketData.items.findIndex(
|
|
(item) => item.product.id === product.id && item.billingCycle === billingCycle,
|
|
)
|
|
|
|
let newItems
|
|
if (existingItemIndex > -1) {
|
|
newItems = [...basketData.items]
|
|
newItems[existingItemIndex].quantity += quantity
|
|
newItems[existingItemIndex] = withItemVat(
|
|
product,
|
|
newItems[existingItemIndex].quantity,
|
|
billingCycle,
|
|
basketData.globalPeriod,
|
|
)
|
|
} else {
|
|
const actualQuantity = !product.isQuantityBased ? 1 : quantity
|
|
newItems = [
|
|
...basketData.items,
|
|
withItemVat(product, actualQuantity, billingCycle, basketData.globalPeriod),
|
|
]
|
|
}
|
|
|
|
const updatedState = { ...basketData, items: newItems, ...calculateBasketTotals(newItems) }
|
|
return updatedState
|
|
}
|
|
|
|
export const removeItemFromBasket = (basketData: BasketData, id: string): BasketData => {
|
|
const newItems = basketData.items.filter((item) => item.product.id !== id)
|
|
const updatedState = { ...basketData, items: newItems, ...calculateBasketTotals(newItems) }
|
|
return updatedState
|
|
}
|
|
|
|
export const updateBasketItemQuantity = (
|
|
basketData: BasketData,
|
|
id: string,
|
|
quantity: number,
|
|
): BasketData => {
|
|
const newItems = basketData.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, basketData.globalPeriod)
|
|
}
|
|
return item
|
|
})
|
|
.filter(Boolean) as BasketItem[]
|
|
|
|
const updatedState = { ...basketData, items: newItems, ...calculateBasketTotals(newItems) }
|
|
return updatedState
|
|
}
|
|
|
|
export const clearBasket = (): BasketData => {
|
|
return initialBasketData
|
|
}
|
|
|
|
export const setBasketGlobalBillingCycle = (basketData: BasketData, cycle: BillingCycle): BasketData => {
|
|
const newItems = basketData.items.map((item) =>
|
|
withItemVat(item.product, item.quantity, cycle, basketData.globalPeriod),
|
|
)
|
|
const updatedState = {
|
|
...basketData,
|
|
globalBillingCycle: cycle,
|
|
items: newItems,
|
|
...calculateBasketTotals(newItems),
|
|
}
|
|
return updatedState
|
|
}
|
|
|
|
export const setBasketGlobalPeriod = (basketData: BasketData, period: number): BasketData => {
|
|
const newItems = basketData.items.map((item) =>
|
|
withItemVat(item.product, item.quantity, item.billingCycle, period),
|
|
)
|
|
const updatedState = {
|
|
...basketData,
|
|
globalPeriod: period,
|
|
items: newItems,
|
|
...calculateBasketTotals(newItems),
|
|
}
|
|
return updatedState
|
|
}
|
|
|
|
export const isItemInBasket = (
|
|
basketData: BasketData,
|
|
productId: string,
|
|
billingCycle: BillingCycle,
|
|
): boolean => {
|
|
return basketData.items.some(
|
|
(item) => item.product.id === productId && item.billingCycle === billingCycle,
|
|
)
|
|
}
|
|
|
|
export const isItemInBasketData = (
|
|
items: BasketItem[],
|
|
productId: string,
|
|
billingCycle: BillingCycle,
|
|
): boolean => {
|
|
return items.some((item) => item.product.id === productId && item.billingCycle === billingCycle)
|
|
}
|