434 lines
24 KiB
JavaScript
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: [] })
|
|
})
|