export interface ProductPricing { price: number; benefitAmount?: number | null; } export function calcBenefitAmount(product: ProductPricing): number { return product.benefitAmount ?? product.price; } export function validateMinPurchase( deliveryType: 'LOCAL' | 'CROSS_CITY', quantity: number, localMinQty: number, crossMinQty: number, ): { ok: boolean; message?: string } { const min = deliveryType === 'LOCAL' ? localMinQty : crossMinQty; if (quantity < min) { return { ok: false, message: deliveryType === 'LOCAL' ? `同城配送至少购买 ${min} 瓶` : `跨城配送至少购买 ${min} 瓶(1箱)`, }; } return { ok: true }; } export function validateRedeemAmount( balance: number, amount: number, documentAmount?: number | null, ): { ok: boolean; message?: string } { if (amount <= 0) return { ok: false, message: '核销金额必须大于 0' }; if (amount > balance) return { ok: false, message: '核销金额不能超过可用余额' }; if (documentAmount != null && amount > documentAmount) { return { ok: false, message: '核销金额不能超过该核销单可用金额' }; } return { ok: true }; } export interface BenefitCouponBalance { id: string; balance: number; createdAt: number; } /** FIFO 跨多张权益券分配核销金额(购酒按单发券,展示为总余额) */ export function allocateBenefitCoupons( coupons: BenefitCouponBalance[], amount: number, documentAmount?: number | null, ): { ok: true; allocations: Array<{ couponId: string; amount: number }> } | { ok: false; message: string } { const active = coupons .filter((c) => c.balance > 0) .sort((a, b) => a.createdAt - b.createdAt); const totalBalance = active.reduce((sum, c) => sum + c.balance, 0); const check = validateRedeemAmount(totalBalance, amount, documentAmount); if (!check.ok) return { ok: false, message: check.message! }; let remaining = amount; const allocations: Array<{ couponId: string; amount: number }> = []; for (const coupon of active) { if (remaining <= 0) break; const take = Math.min(coupon.balance, remaining); allocations.push({ couponId: coupon.id, amount: take }); remaining = Math.round((remaining - take) * 100) / 100; } if (remaining > 0) { return { ok: false, message: '权益余额不足' }; } return { ok: true, allocations }; } export function calcBenefitSummary( balances: number[], ): { totalBalance: number; maxRedeemAmount: number; activeCouponCount: number } { const totalBalance = Math.round(balances.reduce((sum, b) => sum + b, 0) * 100) / 100; return { totalBalance, maxRedeemAmount: totalBalance, activeCouponCount: balances.length, }; } export function calcRedeemSettleAmount(amount: number, settlementRate: number): number { return Math.round(amount * settlementRate * 100) / 100; } export function generateUserNo(): string { const suffix = Math.floor(10000000 + Math.random() * 90000000); return `DK${suffix}`; } export function generateOrderNo(): string { const now = new Date(); const y = now.getFullYear(); const m = String(now.getMonth() + 1).padStart(2, '0'); const d = String(now.getDate()).padStart(2, '0'); const rand = String(Math.floor(Math.random() * 100000)).padStart(5, '0'); return `DK${y}${m}${d}${rand}`; } export function generateCouponNo(): string { return `BC${Date.now()}${Math.floor(Math.random() * 1000)}`; } export function generateRedeemNo(): string { return `RD${Date.now()}${Math.floor(Math.random() * 1000)}`; } export function orderTabToStatuses(tab: string): string[] | undefined { switch (tab) { case 'pending_pay': return ['PENDING_PAY']; case 'pending_ship': return ['PENDING_SHIP', 'OUT_WAREHOUSE']; case 'pending_receive': return ['SHIPPING', 'PENDING_RECEIVE']; case 'completed': return ['COMPLETED']; default: return undefined; } }