From 516a9431f0e0d10d5579422612a76b7c734f157d Mon Sep 17 00:00:00 2001 From: zoujing Date: Thu, 30 Jul 2026 16:15:37 +0800 Subject: [PATCH] fix: harden ticket model edge cases --- src/pages-service/tickets/model.mjs | 10 +++++++--- src/pages-service/tickets/model.test.mjs | 21 +++++++++++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/pages-service/tickets/model.mjs b/src/pages-service/tickets/model.mjs index 1c495c1..72f398e 100644 --- a/src/pages-service/tickets/model.mjs +++ b/src/pages-service/tickets/model.mjs @@ -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) diff --git a/src/pages-service/tickets/model.test.mjs b/src/pages-service/tickets/model.test.mjs index d47bdbf..ea703aa 100644 --- a/src/pages-service/tickets/model.test.mjs +++ b/src/pages-service/tickets/model.test.mjs @@ -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: [] })