Files
YGChatCS/src/pages-service/tickets/model.test.mjs
T
2026-08-06 14:59:03 +08:00

434 lines
24 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import {
TICKET_VIEWS,
applyRefund,
calculateAge,
calculateBookingTotals,
calculateRefundAmount,
createBookingDraft,
createPaidOrder,
isTravelerEligible,
popViewHistory,
validateBooking,
} from './model.mjs'
test('provides coherent default visitor ticket data', async () => {
const data = await import('./data.js')
const { AVAILABLE_DATES, ENTITLEMENT_STATUS_META, GROUPS, INITIAL_TRAVELERS, INSURANCE, ORDER_STATUS_META, PLACEHOLDER_IMAGE, PRODUCT_IMAGES, PRODUCTS, REFUND_REASONS, SCENIC_HERO_IMAGE, SCENIC_SPOT } = data
const { INITIAL_ORDERS } = data
assert.equal(PLACEHOLDER_IMAGE, 'https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/oneFeel/2082673896938569729.jpg')
assert.deepEqual(SCENIC_SPOT, {
id: 'libo-xiaoqikong', name: '荔波小七孔景区', level: '国家5A级景区', heritage: '世界自然遗产', address: '贵州省黔南布依族苗族自治州荔波县', phone: '0854-3619810', latitude: 25.2534, longitude: 107.7255, heroImage: SCENIC_HERO_IMAGE,
})
assert.deepEqual(GROUPS.map(({ id, name, sortOrder, enabled, productIds }) => ({ id, name, sortOrder, enabled, productIds })), [
{ id: 'ticket', name: '景点门票', sortOrder: 1, enabled: true, productIds: ['adult', 'elderly', 'minor', 'free'] },
{ id: 'vip', name: 'VIP速通', sortOrder: 2, enabled: true, productIds: ['vip-fast'] },
{ id: 'bus', name: '直通车', sortOrder: 3, enabled: true, productIds: ['direct-bus'] },
{ id: 'package', name: '门票+观光车', sortOrder: 4, enabled: true, productIds: ['classic'] },
{ id: 'tour', name: '一日游', sortOrder: 5, enabled: true, productIds: ['day-tour'] },
{ id: 'play', name: '玩乐体验', sortOrder: 6, enabled: true, productIds: ['boat', 'guide'] },
])
assert.deepEqual(PRODUCTS.map(({ id }) => id), ['adult', 'elderly', 'minor', 'free', 'vip-fast', 'direct-bus', 'classic', 'day-tour', 'boat', 'guide'])
const productsById = new Map(PRODUCTS.map((product) => [product.id, product]))
assert.ok(GROUPS.every((group) => group.productIds.every((productId) => productsById.has(productId))))
assert.deepEqual(GROUPS.flatMap((group) => group.productIds).sort(), PRODUCTS.map(({ id }) => id).sort())
const productKeys = ['id', 'name', 'shortTitle', 'category', 'description', 'salePrice', 'marketPrice', 'stock', 'sold', 'status', 'clientVisible', 'highlights', 'ageRule', 'included', 'excluded', 'detail', 'purchaseNotice', 'images', 'reservation', 'entitlements']
const reservationKeys = ['requiresDate', 'advanceDays', 'requiresTimeSlot', 'timeSlots', 'realNameRequired', 'idTypes', 'maxQuantity', 'contactPhoneRequired']
const entitlementKeys = ['id', 'name', 'fulfillment', 'quantity', 'unit', 'refundAllocation']
assert.ok(PRODUCTS.every((product) => productKeys.every((key) => key in product) && ['on_sale', 'draft'].includes(product.status) && Number.isFinite(product.salePrice) && Number.isFinite(product.stock) && Number.isFinite(product.sold) && product.images.length === 1 && reservationKeys.every((key) => key in product.reservation) && product.entitlements.length && product.entitlements.every((entitlement) => entitlementKeys.every((key) => key in entitlement))))
assert.deepEqual(PRODUCTS.filter(({ status, clientVisible }) => status !== 'on_sale' || clientVisible === false).map(({ id }) => id), ['guide'])
const visibleProducts = PRODUCTS.filter(({ status, clientVisible }) => status === 'on_sale' && clientVisible !== false)
assert.deepEqual(Object.keys(PRODUCT_IMAGES), visibleProducts.map(({ id }) => id))
assert.ok(SCENIC_HERO_IMAGE.endsWith('/hero-bridge.jpg'))
assert.notEqual(SCENIC_HERO_IMAGE, PLACEHOLDER_IMAGE)
assert.ok(visibleProducts.every((product) => product.images[0] === PRODUCT_IMAGES[product.id] && product.images[0].startsWith('https://one-feel-config-images-bucket.oss-cn-chengdu.aliyuncs.com/xiaoqi/tickets/')))
assert.equal(new Set([SCENIC_HERO_IMAGE, ...visibleProducts.map(({ images }) => images[0])]).size, 10)
assert.equal(productsById.get('guide').images[0], PLACEHOLDER_IMAGE)
assert.deepEqual(PRODUCTS.map(({ id, salePrice, marketPrice, sold }) => [id, salePrice, marketPrice, sold]), [
['adult', 160, 160, 2386], ['elderly', 100, 100, 816], ['minor', 100, 100, 629], ['free', 40, 40, 346], ['vip-fast', 98, 98, 126], ['direct-bus', 68, 68, 92], ['classic', 188, 210, 184], ['day-tour', 188, 188, 68], ['boat', 50, 60, 326], ['guide', 68, 88, 0],
])
assert.deepEqual(productsById.get('elderly').ageRule, { min: 60, max: 999 })
assert.deepEqual(productsById.get('minor').ageRule, { min: 6, max: 17 })
assert.deepEqual(productsById.get('free').ageRule, { min: 0, max: 120 })
assert.deepEqual(productsById.get('adult').entitlements.map(({ fulfillment, refundAllocation }) => [fulfillment, refundAllocation]), [['third_party', 120], ['third_party', 40]])
assert.deepEqual(productsById.get('classic').entitlements.map(({ fulfillment, refundAllocation }) => [fulfillment, refundAllocation]), [['third_party', 120], ['third_party', 40], ['self', 28]])
for (const productId of ['vip-fast', 'day-tour', 'boat', 'guide']) assert.ok(productsById.get(productId).entitlements.every(({ fulfillment }) => fulfillment === 'self'))
for (const productId of ['direct-bus', 'free', 'elderly', 'minor']) assert.ok(productsById.get(productId).entitlements.every(({ fulfillment }) => fulfillment === 'third_party'))
const standardSlots = ['08:00—09:30', '09:30—11:30', '11:30—13:30', '13:30—15:00', '15:00—16:00']
for (const productId of ['adult', 'elderly', 'minor', 'free', 'classic']) assert.deepEqual(productsById.get(productId).reservation.timeSlots, standardSlots)
assert.deepEqual(productsById.get('vip-fast').reservation.timeSlots, ['08:00—10:00', '10:00—12:00', '12:00—14:00', '14:00—16:00'])
assert.deepEqual(productsById.get('direct-bus').reservation.timeSlots, ['06:30—07:00 去程', '18:00—18:30 返程'])
assert.deepEqual(productsById.get('day-tour').reservation.timeSlots, ['08:00—08:30 集合', '09:00—09:30 集合'])
assert.deepEqual(productsById.get('guide').reservation.timeSlots, ['09:00—10:30', '13:30—15:00'])
assert.equal(productsById.get('boat').reservation.requiresTimeSlot, false)
assert.deepEqual(productsById.get('boat').reservation.timeSlots, [])
assert.deepEqual(AVAILABLE_DATES, [
{ date: '2026-08-01', week: '周六', label: '8月1日', stock: 120 }, { date: '2026-08-02', week: '周日', label: '8月2日', stock: 96 }, { date: '2026-08-03', week: '周一', label: '8月3日', stock: 68 }, { date: '2026-08-04', week: '周二', label: '8月4日', stock: 0 }, { date: '2026-08-05', week: '周三', label: '8月5日', stock: 42 },
])
assert.deepEqual(INSURANCE, { name: '出行意外险', price: 5, description: '最高10万元意外保障' })
assert.deepEqual(REFUND_REASONS, ['行程有变,无法按计划出行', '重复购买', '选错日期或票种', '其他原因'])
assert.deepEqual(INITIAL_TRAVELERS, [
{ id: 'traveler-lin-xiao', name: '林晓', idType: '身份证', idNumber: '110101199001020010', phone: '13800138000' }, { id: 'traveler-zhou-ning', name: '周宁', idType: '身份证', idNumber: '110101196001020017', phone: '13900139000' },
])
assert.deepEqual(productsById.get('adult').reservation, { requiresDate: true, advanceDays: 30, requiresTimeSlot: true, timeSlots: standardSlots, realNameRequired: true, idTypes: ['身份证', '护照'], maxQuantity: 5, contactPhoneRequired: true })
assert.deepEqual(productsById.get('adult').highlights, ['官方', '可订明日', '随时退', '无需取票'])
assert.deepEqual(productsById.get('classic').reservation, { requiresDate: true, advanceDays: 30, requiresTimeSlot: true, timeSlots: standardSlots, realNameRequired: true, idTypes: ['身份证', '护照'], maxQuantity: 5, contactPhoneRequired: true })
assert.deepEqual(productsById.get('classic').highlights, ['官方', '实名预约', '整包退', '双凭证'])
assert.deepEqual(INITIAL_TRAVELERS[0], { id: 'traveler-lin-xiao', name: '林晓', idType: '身份证', idNumber: '110101199001020010', phone: '13800138000' })
assert.deepEqual(
Object.fromEntries(Object.entries(ORDER_STATUS_META).map(([status, meta]) => [status, meta.label])),
{
pending_payment: '待支付',
closed: '已关闭',
paid: '待使用',
fulfilling: '出票中',
partial_used: '部分使用',
completed: '已完成',
refunding: '退款中',
partial_refunded: '部分退款',
refunded: '已退款',
exception: '异常',
},
)
assert.deepEqual(
Object.fromEntries(Object.entries(ENTITLEMENT_STATUS_META).map(([status, meta]) => [status, meta.label])),
{ pending: '待使用', used: '已使用', refunding: '退款中', refunded: '已退款' },
)
assert.equal(INITIAL_ORDERS.length, 11)
assert.deepEqual(
INITIAL_ORDERS.map(({ orderNo, productName, status, amount }) => [orderNo, productName, status, amount]),
[
['LB202607260001', '小七孔经典畅游套餐', 'partial_used', 188],
['LB202607260002', '小七孔经典畅游套餐', 'paid', 376],
['LB202607260003', '荔波小七孔景区成人票', 'exception', 120],
['LB202607260004', '鸳鸯湖游船票', 'paid', 100],
['LB202607260005', '荔波小七孔景区成人票', 'fulfilling', 120],
['LB202607260006', '鸳鸯湖游船票', 'completed', 50],
['LB202607260007', '小七孔观光车票', 'refunding', 80],
['LB202607260008', '精品讲解服务', 'refunded', 68],
['LB202607260009', '小七孔经典畅游套餐', 'pending_payment', 188],
['LB202607260010', '荔波小七孔景区成人票', 'closed', 120],
['LB202607260011', '鸳鸯湖游船票', 'partial_refunded', 100],
],
)
assert.deepEqual(new Set(INITIAL_ORDERS.map(({ status }) => status)), new Set(Object.keys(ORDER_STATUS_META)))
for (const order of INITIAL_ORDERS) {
assert.equal(order.paidAmount, order.amount)
assert.equal(order.productAmount, order.amount)
assert.equal(order.insuranceAmount, 0)
assert.equal(order.travelers[0].name, order.customerName)
assert.ok(order.entitlements.every((entitlement) => ['pending', 'used', 'refunding', 'refunded'].includes(entitlement.status)))
}
})
test('honors each product reservation requirement flag when validating bookings', async () => {
const { PRODUCTS } = await import('./data.js')
const boat = PRODUCTS.find(({ id }) => id === 'boat')
const boatDraft = {
...createBookingDraft(boat),
visitDate: '2026-08-01',
contactName: '林晓',
phone: '13800138000',
}
assert.equal(validateBooking({ product: boat, draft: boatDraft, travelers: [] }), '')
const noRequirements = {
...boat,
reservation: {
...boat.reservation,
requiresDate: false,
requiresTimeSlot: false,
realNameRequired: false,
contactPhoneRequired: false,
},
}
assert.equal(validateBooking({ product: noRequirements, draft: createBookingDraft(noRequirements), travelers: [] }), '')
const timeRequired = { ...noRequirements, reservation: { ...noRequirements.reservation, requiresTimeSlot: true } }
assert.equal(validateBooking({ product: timeRequired, draft: createBookingDraft(timeRequired), travelers: [] }), '请选择使用场次')
const contactRequired = { ...noRequirements, reservation: { ...noRequirements.reservation, contactPhoneRequired: true } }
assert.equal(validateBooking({ product: contactRequired, draft: createBookingDraft(contactRequired), travelers: [] }), '请填写正确的联系人和手机号')
})
test('keeps ticket source prototypes, valid traveler IDs, and entitlement allocations aligned', async () => {
const { INITIAL_TRAVELERS, PRODUCTS } = await import('./data.js')
const productsById = new Map(PRODUCTS.map((product) => [product.id, product]))
assert.equal(productsById.get('adult').ageRule, null)
assert.deepEqual(productsById.get('adult').name, '荔波小七孔成人票+观光车')
assert.deepEqual(productsById.get('elderly').name, '荔波小七孔老年人票+观光车')
assert.deepEqual(productsById.get('minor').name, '荔波小七孔未成年票+观光车')
assert.deepEqual(productsById.get('free').name, '荔波小七孔免门票+观光车')
assert.deepEqual(productsById.get('classic').name, '小七孔经典畅游套餐')
assert.deepEqual(productsById.get('boat').name, '鸳鸯湖游船票')
assert.deepEqual(productsById.get('day-tour').name, '荔波小七孔精品一日游')
assert.deepEqual(productsById.get('guide').name, '小七孔精品讲解服务')
assert.deepEqual(productsById.get('direct-bus').name, '贵阳往返小七孔景区直通车')
assert.deepEqual(productsById.get('elderly').reservation.idTypes, ['身份证'])
assert.deepEqual(productsById.get('minor').reservation.idTypes, ['身份证'])
assert.ok(PRODUCTS.filter((product) => product.ageRule).every((product) => product.reservation.idTypes.length === 1 && product.reservation.idTypes[0] === '身份证'))
assert.ok(productsById.get('direct-bus').included.every((item) => item.includes('往返')))
assert.match(productsById.get('direct-bus').detail, /乘车凭证/)
assert.match(productsById.get('direct-bus').purchaseNotice, /提前20分钟/)
assert.deepEqual(productsById.get('adult').entitlements.map(({ name, fulfillment, refundAllocation }) => [name, fulfillment, refundAllocation]), [['小七孔成人门票', 'third_party', 120], ['小七孔观光车票', 'third_party', 40]])
assert.deepEqual(productsById.get('elderly').entitlements.map(({ refundAllocation }) => refundAllocation), [60, 40])
assert.deepEqual(productsById.get('minor').entitlements.map(({ refundAllocation }) => refundAllocation), [60, 40])
assert.deepEqual(productsById.get('free').entitlements.map(({ refundAllocation }) => refundAllocation), [0, 40])
assert.deepEqual(productsById.get('classic').entitlements.map(({ name, fulfillment, refundAllocation }) => [name, fulfillment, refundAllocation]), [['小七孔成人门票', 'third_party', 120], ['观光车票', 'third_party', 40], ['鸳鸯湖游船', 'self', 28]])
assert.ok(PRODUCTS.every((product) => product.entitlements.every((entitlement) => Number.isFinite(entitlement.quantity) && entitlement.quantity > 0 && Number.isFinite(entitlement.refundAllocation)) && product.entitlements.reduce((total, entitlement) => total + entitlement.refundAllocation, 0) === product.salePrice))
assert.deepEqual(INITIAL_TRAVELERS[1], { id: 'traveler-zhou-ning', name: '周宁', idType: '身份证', idNumber: '110101196001020017', phone: '13900139000' })
})
const adultProduct = {
id: 'adult',
name: '成人票',
salePrice: 160,
reservation: {
maxQuantity: 5,
},
ageRule: {
min: 18,
max: 59,
},
entitlements: [
{
id: 'adult-ticket',
name: '成人门票',
fulfillment: 'third_party',
quantity: 1,
unit: '张',
refundAllocation: 120,
},
{
id: 'shuttle',
name: '观光车票',
fulfillment: 'self',
quantity: 1,
unit: '张',
refundAllocation: 40,
},
],
}
const eligibleTraveler = {
id: 't-1',
name: '林晓',
idType: '身份证',
idNumber: '110101199001020010',
}
test('exposes the ticket views', () => {
assert.deepEqual(TICKET_VIEWS, {
HOME: 'home',
DETAIL: 'detail',
BOOKING: 'booking',
TRAVELERS: 'travelers',
PAID: 'paid',
ORDERS: 'orders',
ORDER_DETAIL: 'orderDetail',
REFUND: 'refundApply',
REFUND_RESULT: 'refundResult',
CREDENTIALS: 'credentials',
})
})
test('calculates age from a mainland ID at the visit date', () => {
assert.equal(calculateAge('110101196001010010', '2026-08-01'), 66)
assert.equal(calculateAge('110101201001020010', '2026-01-01'), 15)
assert.equal(calculateAge('not-an-id', '2026-08-01'), null)
})
test('checks traveler eligibility against an age rule', () => {
const traveler = {
idType: '身份证',
idNumber: '110101201001020010',
}
assert.equal(isTravelerEligible(traveler, { ageRule: { min: 6, max: 17 } }, '2026-08-01'), true)
assert.equal(isTravelerEligible(traveler, { ageRule: { min: 60, max: 120 } }, '2026-08-01'), false)
assert.equal(isTravelerEligible({ idType: '护照' }, {}, '2026-08-01'), true)
assert.equal(isTravelerEligible({ idType: '护照' }, { ageRule: { min: 6, max: 17 } }, '2026-08-01'), false)
assert.equal(isTravelerEligible({ idType: '身份证', idNumber: '110101202008010010' }, { ageRule: { min: 6, max: 17 } }, '2026-08-01'), true)
assert.equal(isTravelerEligible({ idType: '身份证', idNumber: '110101200908010010' }, { ageRule: { min: 6, max: 17 } }, '2026-08-01'), true)
})
test('calculates booking totals including selected insurance', () => {
assert.deepEqual(calculateBookingTotals(adultProduct, 2, true, 5), {
productAmount: 320,
insuranceAmount: 10,
paidAmount: 330,
})
})
test('validates booking draft fields and selected travelers in order', () => {
const draft = createBookingDraft(adultProduct)
const incompleteTraveler = { id: 't-1', idType: '身份证', idNumber: '' }
const childTraveler = { id: 't-1', name: '小林', idType: '身份证', idNumber: '110101201001020010' }
assert.deepEqual(draft, {
visitDate: '',
timeSlot: '',
quantity: 1,
travelerIds: [],
contactName: '',
phone: '',
insuranceSelected: false,
insurancePrice: 5,
maxQuantity: 5,
})
assert.equal(validateBooking({ product: adultProduct, draft, travelers: [] }), '请选择使用日期')
const completeDraft = {
...draft,
visitDate: '2026-08-01',
timeSlot: '08:00—09:30',
quantity: 1,
travelerIds: ['t-1'],
contactName: '林晓',
phone: '13800138000',
}
assert.equal(validateBooking({ product: adultProduct, draft: { ...completeDraft, timeSlot: '' }, travelers: [eligibleTraveler] }), '请选择使用场次')
assert.equal(validateBooking({ product: adultProduct, draft: { ...completeDraft, quantity: 0 }, travelers: [eligibleTraveler] }), '购买数量无效')
assert.equal(validateBooking({ product: adultProduct, draft: { ...completeDraft, maxQuantity: 0 }, travelers: [eligibleTraveler] }), '购买数量无效')
assert.equal(validateBooking({ product: adultProduct, draft: { ...completeDraft, travelerIds: [] }, travelers: [eligibleTraveler] }), '请选择足够数量的出行人')
assert.equal(validateBooking({ product: adultProduct, draft: { ...completeDraft, quantity: 2, travelerIds: ['t-1', 't-1'] }, travelers: [eligibleTraveler] }), '请选择足够数量的出行人')
assert.equal(validateBooking({ product: adultProduct, draft: completeDraft, travelers: [incompleteTraveler] }), '请完善出行人实名信息')
assert.equal(validateBooking({ product: adultProduct, draft: completeDraft, travelers: [childTraveler] }), '出行人年龄不符合票种要求')
assert.equal(validateBooking({ product: adultProduct, draft: { ...completeDraft, phone: '123' }, travelers: [eligibleTraveler] }), '请填写正确的联系人和手机号')
assert.equal(validateBooking({ product: adultProduct, draft: completeDraft, travelers: [eligibleTraveler] }), '')
})
test('creates a paid order with entitlements and credentials', () => {
const now = new Date('2026-07-30T10:00:00+08:00')
const order = createPaidOrder({
product: adultProduct,
draft: {
...createBookingDraft(adultProduct),
visitDate: '2026-08-01',
timeSlot: '08:00—09:30',
travelerIds: ['t-1'],
contactName: '林晓',
phone: '13800138000',
insuranceSelected: true,
},
travelers: [eligibleTraveler],
now,
})
assert.equal(order.status, 'paid')
assert.equal(order.paidAmount, 165)
assert.equal(order.entitlements.length, 2)
assert.equal(order.product, undefined)
assert.equal(order.productId, 'adult')
assert.equal(order.productName, '成人票')
assert.equal(order.id, `ORDER-${now.getTime()}`)
assert.equal(order.orderNo, order.id)
assert.equal(order.paidAt, '2026-07-30 10:00:00')
assert.deepEqual(order.gateCredential, { status: 'issued', qrToken: `GATE-${order.orderNo}` })
assert.deepEqual(order.selfCredential, { status: 'issued', qrToken: `SELF-${order.orderNo}` })
assert.deepEqual(order.insurance, { name: '出行意外险', price: 5, quantity: 1, status: 'active' })
assert.deepEqual(order.logs[0], { time: order.paidAt, text: '订单支付成功', operator: '系统' })
const secondTraveler = { ...eligibleTraveler, id: 't-2', idNumber: '110101199101020010' }
const thirdTraveler = { ...eligibleTraveler, id: 't-3', idNumber: '110101199201020010' }
const uninsuredOrder = createPaidOrder({
product: adultProduct,
draft: {
...createBookingDraft(adultProduct),
visitDate: '2026-08-01',
timeSlot: '08:00—09:30',
quantity: 2,
travelerIds: ['t-1', 't-2'],
contactName: '林晓',
phone: '13800138000',
},
travelers: [eligibleTraveler, secondTraveler],
now,
})
assert.equal(uninsuredOrder.travelers.length, 2)
assert.deepEqual(uninsuredOrder.entitlements.map((entitlement) => entitlement.quantity), [2, 2])
assert.deepEqual(uninsuredOrder.entitlements.map((entitlement) => entitlement.status), ['pending', 'pending'])
assert.equal(uninsuredOrder.insurance, null)
const insuredOrder = createPaidOrder({
product: adultProduct,
draft: {
...createBookingDraft(adultProduct),
visitDate: '2026-08-01',
timeSlot: '08:00—09:30',
quantity: 2,
travelerIds: ['t-1', 't-2', 't-3'],
contactName: '林晓',
phone: '13800138000',
insuranceSelected: true,
},
travelers: [eligibleTraveler, secondTraveler, thirdTraveler],
now,
})
assert.deepEqual(insuredOrder.travelers.map((traveler) => traveler.id), ['t-1', 't-2'])
assert.deepEqual(insuredOrder.entitlements.map((entitlement) => entitlement.quantity), [2, 2])
assert.deepEqual(insuredOrder.entitlements.map((entitlement) => entitlement.refundAllocation), [120, 40])
assert.deepEqual(insuredOrder.entitlements.map((entitlement) => entitlement.usedQuantity), [0, 0])
assert.equal(calculateRefundAmount(insuredOrder, [insuredOrder.entitlements[0].id]), 240)
assert.notEqual(insuredOrder.entitlements[0], adultProduct.entitlements[0])
assert.deepEqual(adultProduct.entitlements, [
{ id: 'adult-ticket', name: '成人门票', fulfillment: 'third_party', quantity: 1, unit: '张', refundAllocation: 120 },
{ id: 'shuttle', name: '观光车票', fulfillment: 'self', quantity: 1, unit: '张', refundAllocation: 40 },
])
assert.deepEqual(insuredOrder.insurance, { name: '出行意外险', price: 5, quantity: 2, status: 'active' })
const zeroQuantityOrder = createPaidOrder({
product: { ...adultProduct, entitlements: [{ ...adultProduct.entitlements[0], quantity: 0 }] },
draft: { ...createBookingDraft(adultProduct), quantity: 2 },
travelers: [],
now,
})
assert.equal(zeroQuantityOrder.entitlements[0].quantity, 0)
})
test('applies refunds and pops view history without mutation', () => {
const order = createPaidOrder({
product: adultProduct,
draft: {
...createBookingDraft(adultProduct),
visitDate: '2026-08-01',
timeSlot: '08:00—09:30',
travelerIds: ['t-1'],
contactName: '林晓',
phone: '13800138000',
},
travelers: [eligibleTraveler],
})
const entitlementId = order.entitlements[0].id
const history = ['home', 'detail', 'booking']
const originalOrder = structuredClone(order)
assert.equal(calculateRefundAmount(order, [entitlementId]), 120)
const result = applyRefund(order, [entitlementId], '行程有变', new Date('2026-07-30T10:01:00+08:00'))
assert.equal(result.order.status, 'refunding')
assert.equal(result.refund.amount, 120)
assert.equal(result.order.entitlements[0].status, 'refunding')
assert.deepEqual(order, originalOrder)
assert.notEqual(result.order.entitlements, order.entitlements)
assert.notEqual(result.order.logs, order.logs)
assert.deepEqual(result.order.logs.at(-1), { time: '2026-07-30 10:01:00', text: '已申请退款', operator: '用户' })
const repeatedRefund = applyRefund(result.order, [entitlementId, 'missing-entitlement'], '重复申请')
assert.equal(repeatedRefund.order, result.order)
assert.equal(repeatedRefund.refund, null)
const emptyRefund = applyRefund(order, [], '空申请')
assert.equal(emptyRefund.order, order)
assert.equal(emptyRefund.refund, null)
assert.deepEqual(popViewHistory(history), { view: 'booking', history: ['home', 'detail'] })
assert.deepEqual(history, ['home', 'detail', 'booking'])
assert.deepEqual(popViewHistory([]), { view: 'home', history: [] })
})