4372018c09
Refund rollback, winery T+3, multi withdraw, mini-user store detail, dev plan batch edit and WeCom dispatch, support ticket edit/attachments/batch status, package imageUrl. Co-authored-by: Cursor <cursoragent@cursor.com>
289 lines
8.6 KiB
TypeScript
289 lines
8.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import {
|
||
calcBenefitAmount,
|
||
calcRedeemSettleAmount,
|
||
calcLogisticsFeeByBottles,
|
||
calcOrderBoxCount,
|
||
shouldHoldAutoCourierDispatch,
|
||
validateMinPurchase,
|
||
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);
|
||
});
|
||
});
|
||
|
||
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);
|
||
});
|
||
|
||
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('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);
|
||
});
|
||
});
|