feat: add ticket booking domain model

This commit is contained in:
2026-07-30 15:54:13 +08:00
parent 9487dad416
commit 578e186746
2 changed files with 366 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
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'
const adultProduct = {
id: 'adult',
name: '成人票',
salePrice: 160,
reservation: {
maxQuantity: 5,
},
ageRule: {
min: 18,
max: 59,
},
entitlements: [
{
id: 'adult-ticket',
name: '成人门票',
allocation: 120,
third_party: true,
},
{
id: 'shuttle',
name: '摆渡车',
allocation: 40,
self: true,
},
],
}
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)
})
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)
})
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.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, 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 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: new Date('2026-07-30T10:00:00+08:00'),
})
assert.equal(order.status, 'paid')
assert.equal(order.paidAmount, 165)
assert.equal(order.entitlements.length, 2)
assert.match(order.gateCredential.qrToken, /^GATE-/)
assert.equal(order.logs[0].message, '订单支付成功')
})
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']
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(applyRefund(result.order, [entitlementId], '重复申请').refund.entitlementIds, [])
assert.deepEqual(popViewHistory(history), { view: 'booking', history: ['home', 'detail'] })
assert.deepEqual(history, ['home', 'detail', 'booking'])
assert.deepEqual(popViewHistory([]), { view: 'home', history: [] })
})