fix: harden ticket model edge cases

This commit is contained in:
2026-07-30 16:15:37 +08:00
parent cb8cbb66f7
commit 516a9431f0
2 changed files with 26 additions and 5 deletions

View File

@@ -15,8 +15,9 @@ const toMoney = (value) => Number(Number(value || 0).toFixed(2))
const formatTimestamp = (date) => {
const pad = (value) => String(value).padStart(2, '0')
const chinaDate = new Date(date.getTime() + 8 * 60 * 60 * 1000)
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
return `${chinaDate.getUTCFullYear()}-${pad(chinaDate.getUTCMonth() + 1)}-${pad(chinaDate.getUTCDate())} ${pad(chinaDate.getUTCHours())}:${pad(chinaDate.getUTCMinutes())}:${pad(chinaDate.getUTCSeconds())}`
}
const createCredential = (prefix, orderNo) => ({
@@ -91,7 +92,7 @@ export const createBookingDraft = (product) => ({
export const validateBooking = ({ product, draft, travelers }) => {
if (!draft?.visitDate) return '请选择使用日期'
if (!draft.timeSlot) return '请选择使用场次'
if (!Number.isInteger(draft.quantity) || draft.quantity < 1 || (draft.maxQuantity && draft.quantity > draft.maxQuantity)) return '购买数量无效'
if (!Number.isInteger(draft.quantity) || draft.quantity < 1 || (draft.maxQuantity !== undefined && draft.maxQuantity !== null && draft.quantity > draft.maxQuantity)) return '购买数量无效'
const selectedTravelers = [...new Set(draft.travelerIds || [])]
.map((id) => travelers.find((traveler) => traveler.id === id))
@@ -121,7 +122,7 @@ export const createPaidOrder = ({ product, draft, travelers, now = new Date() })
...entitlement,
id: `${orderId}-${entitlement.id || 'entitlement'}-${index + 1}`,
sourceId: entitlement.id,
quantity: (Number(entitlement.quantity) || 1) * draft.quantity,
quantity: Number(entitlement.quantity ?? 1) * draft.quantity,
usedQuantity: 0,
status: 'pending',
}))
@@ -165,6 +166,9 @@ export const applyRefund = (order, entitlementIds, reason, now = new Date()) =>
const refundableIds = order.entitlements
.filter((entitlement) => entitlement.status === 'pending' && selectedIds.has(entitlement.id))
.map((entitlement) => entitlement.id)
if (!refundableIds.length) return { order, refund: null }
const amount = calculateRefundAmount(order, entitlementIds)
const entitlements = order.entitlements.map((entitlement) => (
entitlement.status === 'pending' && selectedIds.has(entitlement.id)

View File

@@ -125,6 +125,7 @@ test('validates booking draft fields and selected travelers in order', () => {
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] }), '请完善出行人实名信息')
@@ -213,6 +214,15 @@ test('creates a paid order with entitlements and credentials', () => {
{ 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', () => {
@@ -230,6 +240,7 @@ test('applies refunds and pops view history without mutation', () => {
})
const entitlementId = order.entitlements[0].id
const history = ['home', 'detail', 'booking']
const originalOrder = structuredClone(order)
assert.equal(calculateRefundAmount(order, [entitlementId]), 120)
@@ -237,10 +248,16 @@ test('applies refunds and pops view history without mutation', () => {
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.refund.amount, 0)
assert.deepEqual(repeatedRefund.refund.entitlementIds, [])
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: [] })