import { describe, expect, it } from 'vitest'; import { calcBenefitAmount, calcRedeemSettleAmount, resolveSettlementRate, calcLogisticsFeeByBottles, calcOrderBoxCount, shouldHoldAutoCourierDispatch, validateMinPurchase, toBottleQuantity, toMinSaleQuantity, validateBusinessHours, formatBusinessHours, validateRedeemAmount, allocateBenefitCoupons, calcBenefitSummary, orderTabToStatuses, classifyRedeemClientError, generateRedeemPendingNo, REDEEM_WEAKNET_FAIL_THRESHOLD, sumUnbilledPayoutAmount, validateStoreWithdraw, pickPayoutsForWithdrawAmount, } from './index'; describe('calcBenefitAmount', () => { it('uses benefitAmount when set', () => { expect(calcBenefitAmount({ price: 599, benefitAmount: 200 })).toBe(200); }); it('falls back to price', () => { expect(calcBenefitAmount({ price: 599 })).toBe(599); }); }); describe('validateMinPurchase', () => { it('local requires 2 bottles', () => { expect(validateMinPurchase('LOCAL', 1, 2, 6).ok).toBe(false); expect(validateMinPurchase('LOCAL', 2, 2, 6).ok).toBe(true); }); it('cross city requires 6 bottles', () => { expect(validateMinPurchase('CROSS_CITY', 5, 2, 6).ok).toBe(false); expect(validateMinPurchase('CROSS_CITY', 6, 2, 6).ok).toBe(true); }); it('on-site pickup requires at least local min (2 bottles)', () => { expect(validateMinPurchase('ON_SITE_PICKUP', 0, 2, 6).ok).toBe(false); expect(validateMinPurchase('ON_SITE_PICKUP', 1, 2, 6).ok).toBe(false); expect(validateMinPurchase('ON_SITE_PICKUP', 2, 2, 6).ok).toBe(true); }); it('box SKU: 1 box (6 bottles) satisfies local and cross mins', () => { const opts = { bottlesPerUnit: 6, saleUnit: 'BOX' as const }; expect(validateMinPurchase('LOCAL', 1, 2, 6, opts).ok).toBe(true); expect(validateMinPurchase('CROSS_CITY', 1, 2, 6, opts).ok).toBe(true); expect(validateMinPurchase('ON_SITE_PICKUP', 1, 2, 6, opts).ok).toBe(true); }); it('box SKU message uses 箱', () => { const r = validateMinPurchase('LOCAL', 0, 2, 6, { bottlesPerUnit: 6, saleUnit: 'BOX' }); expect(r.ok).toBe(false); expect(r.message).toContain('箱'); }); }); describe('toBottleQuantity / toMinSaleQuantity', () => { it('converts sale qty to bottles', () => { expect(toBottleQuantity(2, 1)).toBe(2); expect(toBottleQuantity(1, 6)).toBe(6); expect(toMinSaleQuantity(2, 1)).toBe(2); expect(toMinSaleQuantity(2, 6)).toBe(1); expect(toMinSaleQuantity(6, 6)).toBe(1); expect(toMinSaleQuantity(7, 6)).toBe(2); }); }); describe('validateBusinessHours', () => { it('accepts one segment', () => { expect(validateBusinessHours([{ open: '09:00', close: '22:00' }]).ok).toBe(true); }); it('accepts two non-overlapping segments', () => { expect( validateBusinessHours([ { open: '09:00', close: '14:00' }, { open: '17:00', close: '21:00' }, ]).ok, ).toBe(true); }); it('rejects overlapping second segment', () => { expect( validateBusinessHours([ { open: '09:00', close: '14:00' }, { open: '13:00', close: '21:00' }, ]).ok, ).toBe(false); }); }); describe('formatBusinessHours', () => { it('formats one or two segments', () => { expect(formatBusinessHours({ openTime: '9:00', closeTime: '22:00' })).toBe('9:00-22:00'); expect( formatBusinessHours({ openTime: '09:00', closeTime: '14:00', openTime2: '17:00', closeTime2: '21:00', }), ).toBe('09:00-14:00,17:00-21:00'); }); }); describe('validateRedeemAmount', () => { it('rejects over balance or non-positive amount', () => { expect(validateRedeemAmount(100, 50).ok).toBe(true); expect(validateRedeemAmount(100, 100).ok).toBe(true); expect(validateRedeemAmount(50, 60).ok).toBe(false); expect(validateRedeemAmount(100, 0).ok).toBe(false); expect(validateRedeemAmount(100, Number.NaN).ok).toBe(false); }); it('allows direct redeem up to total balance without a document cap', () => { expect(validateRedeemAmount(700, 650).ok).toBe(true); expect(validateRedeemAmount(700, 701).ok).toBe(false); }); it('caps document-based redeem by document amount', () => { expect(validateRedeemAmount(700, 300, 300).ok).toBe(true); expect(validateRedeemAmount(700, 301, 300)).toEqual({ ok: false, message: '核销金额不能超过该核销单可用金额', }); }); }); describe('calcRedeemSettleAmount', () => { it('applies settlement rate', () => { expect(calcRedeemSettleAmount(100, 0.6)).toBe(60); }); }); describe('resolveSettlementRate', () => { it('falls back to 0.6 when missing or invalid', () => { expect(resolveSettlementRate(0.6)).toBe(0.6); expect(resolveSettlementRate(0)).toBe(0.6); expect(resolveSettlementRate(Number.NaN)).toBe(0.6); expect(resolveSettlementRate(undefined)).toBe(0.6); }); }); describe('calcLogisticsFeeByBottles', () => { const xfx = { baseBottles: 2, baseFee: 6, extraBottleFee: 2, boxBottles: 6, boxFee: 14, }; it('charges base for 1~2 bottles', () => { expect(calcLogisticsFeeByBottles(1, xfx)).toBe(6); expect(calcLogisticsFeeByBottles(2, xfx)).toBe(6); }); it('adds extra bottle fee', () => { expect(calcLogisticsFeeByBottles(3, xfx)).toBe(8); expect(calcLogisticsFeeByBottles(5, xfx)).toBe(12); }); it('uses box fee for full boxes', () => { expect(calcLogisticsFeeByBottles(6, xfx)).toBe(14); expect(calcLogisticsFeeByBottles(12, xfx)).toBe(28); }); it('combines boxes with remainder ladder', () => { expect(calcLogisticsFeeByBottles(7, xfx)).toBe(20); expect(calcLogisticsFeeByBottles(8, xfx)).toBe(20); expect(calcLogisticsFeeByBottles(9, xfx)).toBe(22); }); }); describe('shouldHoldAutoCourierDispatch', () => { it('holds at 10 full boxes (60 bottles)', () => { expect(shouldHoldAutoCourierDispatch(59)).toBe(false); expect(shouldHoldAutoCourierDispatch(60)).toBe(true); expect(shouldHoldAutoCourierDispatch(66)).toBe(true); expect(calcOrderBoxCount(60)).toBe(10); }); }); describe('allocateBenefitCoupons', () => { const coupons = [ { id: '1', balance: 300, createdAt: 1 }, { id: '2', balance: 400, createdAt: 2 }, ]; it('allocates FIFO across coupons', () => { const result = allocateBenefitCoupons(coupons, 500); expect(result.ok).toBe(true); if (result.ok) { expect(result.allocations).toEqual([ { couponId: '1', amount: 300 }, { couponId: '2', amount: 200 }, ]); } }); it('rejects when total balance insufficient', () => { expect(allocateBenefitCoupons(coupons, 800).ok).toBe(false); }); it('rejects when a document cap is lower than requested amount', () => { expect(allocateBenefitCoupons(coupons, 301, 300)).toEqual({ ok: false, message: '核销金额不能超过该核销单可用金额', }); }); }); describe('orderTabToStatuses', () => { it('maps V3.0 three user tabs', () => { expect(orderTabToStatuses('pending_pay')).toEqual(['PENDING_PAY']); expect(orderTabToStatuses('paid')).toEqual([ 'PENDING_SHIP', 'OUT_WAREHOUSE', 'SHIPPING', 'PENDING_RECEIVE', ]); expect(orderTabToStatuses('completed')).toEqual(['COMPLETED']); }); it('keeps legacy tab keys mapped to paid', () => { expect(orderTabToStatuses('pending_ship')).toEqual(orderTabToStatuses('paid')); expect(orderTabToStatuses('pending_receive')).toEqual(orderTabToStatuses('paid')); }); }); describe('calcBenefitSummary', () => { it('max redeem equals total balance', () => { expect(calcBenefitSummary([300, 400])).toEqual({ totalBalance: 700, maxRedeemAmount: 700, activeCouponCount: 2, }); expect(calcBenefitSummary([100])).toEqual({ totalBalance: 100, maxRedeemAmount: 100, activeCouponCount: 1, }); }); }); describe('classifyRedeemClientError', () => { it('treats 5xx and network messages as NETWORK', () => { expect(classifyRedeemClientError({ status: 502 })).toBe('NETWORK'); expect(classifyRedeemClientError({ message: 'Failed to fetch' })).toBe('NETWORK'); expect(classifyRedeemClientError({ message: '网络超时' })).toBe('NETWORK'); }); it('treats business messages as BUSINESS', () => { expect(classifyRedeemClientError({ status: 400, message: '核销码无效或已过期' })).toBe('BUSINESS'); expect(classifyRedeemClientError({ message: '余额不足' })).toBe('BUSINESS'); }); }); describe('generateRedeemPendingNo', () => { it('uses RP prefix', () => { expect(generateRedeemPendingNo().startsWith('RP')).toBe(true); }); it('exports weaknet threshold of 5', () => { expect(REDEEM_WEAKNET_FAIL_THRESHOLD).toBe(5); }); }); describe('sumUnbilledPayoutAmount', () => { it('sums payout amounts', () => { expect(sumUnbilledPayoutAmount([{ payoutAmount: 60 }, { payoutAmount: 40.5 }])).toBe(100.5); }); }); describe('validateStoreWithdraw', () => { const base = { availableAmount: 1000, requestAmount: 200, todayApplied: 0, dailyLimit: 5000, hasBankAccount: true, }; it('rejects when daily limit exceeded', () => { expect( validateStoreWithdraw({ ...base, todayApplied: 4900, requestAmount: 200, dailyLimit: 5000 }).ok, ).toBe(false); }); it('rejects missing bank / over available', () => { expect(validateStoreWithdraw({ ...base, hasBankAccount: false }).ok).toBe(false); expect(validateStoreWithdraw({ ...base, requestAmount: 1001 }).ok).toBe(false); }); it('accepts valid request', () => { expect(validateStoreWithdraw(base).ok).toBe(true); }); }); describe('pickPayoutsForWithdrawAmount', () => { it('picks FIFO exact match', () => { const rows = [{ payoutAmount: 60 }, { payoutAmount: 40 }, { payoutAmount: 30 }]; const r = pickPayoutsForWithdrawAmount(rows, 100); expect(r.ok).toBe(true); if (r.ok) { expect(r.selected).toHaveLength(2); expect(r.amount).toBe(100); } }); it('rejects non-exact match', () => { const rows = [{ payoutAmount: 60 }, { payoutAmount: 40 }]; expect(pickPayoutsForWithdrawAmount(rows, 50).ok).toBe(false); }); });