fix: align ticket order entitlement model
This commit is contained in:
@@ -19,10 +19,9 @@ const formatTimestamp = (date) => {
|
|||||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const createCredential = (prefix, orderId, createdAt) => ({
|
const createCredential = (prefix, orderNo) => ({
|
||||||
type: prefix === 'GATE' ? 'gate' : 'self',
|
status: 'issued',
|
||||||
qrToken: `${prefix}-${orderId}`,
|
qrToken: `${prefix}-${orderNo}`,
|
||||||
issuedAt: createdAt,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const calculateAge = (idNumber, visitDate) => {
|
export const calculateAge = (idNumber, visitDate) => {
|
||||||
@@ -120,18 +119,20 @@ export const createPaidOrder = ({ product, draft, travelers, now = new Date() })
|
|||||||
const totals = calculateBookingTotals(product, draft.quantity, draft.insuranceSelected, draft.insurancePrice)
|
const totals = calculateBookingTotals(product, draft.quantity, draft.insuranceSelected, draft.insurancePrice)
|
||||||
const entitlements = (product.entitlements || []).map((entitlement, index) => ({
|
const entitlements = (product.entitlements || []).map((entitlement, index) => ({
|
||||||
...entitlement,
|
...entitlement,
|
||||||
id: `${entitlement.id || 'entitlement'}-${index + 1}`,
|
id: `${orderId}-${entitlement.id || 'entitlement'}-${index + 1}`,
|
||||||
sourceId: entitlement.id,
|
sourceId: entitlement.id,
|
||||||
quantity: draft.quantity,
|
quantity: (Number(entitlement.quantity) || 1) * draft.quantity,
|
||||||
allocation: toMoney(entitlement.allocation * draft.quantity),
|
usedQuantity: 0,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
}))
|
}))
|
||||||
const hasGateCredential = entitlements.some((entitlement) => entitlement.third_party)
|
const hasGateCredential = entitlements.some((entitlement) => entitlement.fulfillment === 'third_party')
|
||||||
const hasSelfCredential = entitlements.some((entitlement) => entitlement.self)
|
const hasSelfCredential = entitlements.some((entitlement) => entitlement.fulfillment === 'self')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: orderId,
|
id: orderId,
|
||||||
product: { ...product },
|
orderNo: orderId,
|
||||||
|
productId: product.id,
|
||||||
|
productName: product.name,
|
||||||
visitDate: draft.visitDate,
|
visitDate: draft.visitDate,
|
||||||
timeSlot: draft.timeSlot,
|
timeSlot: draft.timeSlot,
|
||||||
quantity: draft.quantity,
|
quantity: draft.quantity,
|
||||||
@@ -139,23 +140,23 @@ export const createPaidOrder = ({ product, draft, travelers, now = new Date() })
|
|||||||
contactName: draft.contactName,
|
contactName: draft.contactName,
|
||||||
phone: draft.phone,
|
phone: draft.phone,
|
||||||
insurance: draft.insuranceSelected
|
insurance: draft.insuranceSelected
|
||||||
? { price: toMoney(draft.insurancePrice), amount: totals.insuranceAmount }
|
? { name: '出行意外险', price: toMoney(draft.insurancePrice), quantity: draft.quantity, status: 'active' }
|
||||||
: undefined,
|
: null,
|
||||||
...totals,
|
...totals,
|
||||||
status: 'paid',
|
status: 'paid',
|
||||||
refundStatus: '',
|
refundStatus: '',
|
||||||
entitlements,
|
entitlements,
|
||||||
...(hasGateCredential ? { gateCredential: createCredential('GATE', orderId, createdAt) } : {}),
|
gateCredential: hasGateCredential ? createCredential('GATE', orderId) : null,
|
||||||
...(hasSelfCredential ? { selfCredential: createCredential('SELF', orderId, createdAt) } : {}),
|
selfCredential: hasSelfCredential ? createCredential('SELF', orderId) : null,
|
||||||
createdAt,
|
paidAt: createdAt,
|
||||||
logs: [{ message: '订单支付成功', createdAt }],
|
logs: [{ time: createdAt, text: '订单支付成功', operator: '系统' }],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const calculateRefundAmount = (order, entitlementIds) => toMoney(
|
export const calculateRefundAmount = (order, entitlementIds) => toMoney(
|
||||||
(order?.entitlements || [])
|
(order?.entitlements || [])
|
||||||
.filter((entitlement) => entitlement.status === 'pending' && entitlementIds.includes(entitlement.id))
|
.filter((entitlement) => entitlement.status === 'pending' && entitlementIds.includes(entitlement.id))
|
||||||
.reduce((total, entitlement) => total + Number(entitlement.allocation || 0), 0),
|
.reduce((total, entitlement) => total + Number(entitlement.refundAllocation || 0) * Number(entitlement.quantity || 0), 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const applyRefund = (order, entitlementIds, reason, now = new Date()) => {
|
export const applyRefund = (order, entitlementIds, reason, now = new Date()) => {
|
||||||
@@ -185,7 +186,7 @@ export const applyRefund = (order, entitlementIds, reason, now = new Date()) =>
|
|||||||
entitlements,
|
entitlements,
|
||||||
status: 'refunding',
|
status: 'refunding',
|
||||||
refundStatus: 'pending',
|
refundStatus: 'pending',
|
||||||
logs: [...(order.logs || []), { message: '已申请退款', createdAt }],
|
logs: [...(order.logs || []), { time: createdAt, text: '已申请退款', operator: '用户' }],
|
||||||
},
|
},
|
||||||
refund,
|
refund,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,16 +29,18 @@ const adultProduct = {
|
|||||||
{
|
{
|
||||||
id: 'adult-ticket',
|
id: 'adult-ticket',
|
||||||
name: '成人门票',
|
name: '成人门票',
|
||||||
allocation: 120,
|
fulfillment: 'third_party',
|
||||||
|
quantity: 1,
|
||||||
|
unit: '张',
|
||||||
refundAllocation: 120,
|
refundAllocation: 120,
|
||||||
third_party: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'shuttle',
|
id: 'shuttle',
|
||||||
name: '摆渡车',
|
name: '观光车票',
|
||||||
allocation: 40,
|
fulfillment: 'self',
|
||||||
|
quantity: 1,
|
||||||
|
unit: '张',
|
||||||
refundAllocation: 40,
|
refundAllocation: 40,
|
||||||
self: true,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -151,14 +153,16 @@ test('creates a paid order with entitlements and credentials', () => {
|
|||||||
assert.equal(order.status, 'paid')
|
assert.equal(order.status, 'paid')
|
||||||
assert.equal(order.paidAmount, 165)
|
assert.equal(order.paidAmount, 165)
|
||||||
assert.equal(order.entitlements.length, 2)
|
assert.equal(order.entitlements.length, 2)
|
||||||
assert.match(order.gateCredential.qrToken, /^GATE-/)
|
assert.equal(order.product, undefined)
|
||||||
assert.match(order.selfCredential.qrToken, /^SELF-/)
|
assert.equal(order.productId, 'adult')
|
||||||
assert.equal(order.createdAt, '2026-07-30 10:00:00')
|
assert.equal(order.productName, '成人票')
|
||||||
assert.equal(order.id, `ORDER-${now.getTime()}`)
|
assert.equal(order.id, `ORDER-${now.getTime()}`)
|
||||||
assert.equal(order.gateCredential.qrToken, `GATE-${order.id}`)
|
assert.equal(order.orderNo, order.id)
|
||||||
assert.equal(order.selfCredential.qrToken, `SELF-${order.id}`)
|
assert.equal(order.paidAt, '2026-07-30 10:00:00')
|
||||||
assert.deepEqual(order.insurance, { price: 5, amount: 5 })
|
assert.deepEqual(order.gateCredential, { status: 'issued', qrToken: `GATE-${order.orderNo}` })
|
||||||
assert.equal(order.logs[0].message, '订单支付成功')
|
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 secondTraveler = { ...eligibleTraveler, id: 't-2', idNumber: '110101199101020010' }
|
||||||
const thirdTraveler = { ...eligibleTraveler, id: 't-3', idNumber: '110101199201020010' }
|
const thirdTraveler = { ...eligibleTraveler, id: 't-3', idNumber: '110101199201020010' }
|
||||||
@@ -180,7 +184,7 @@ test('creates a paid order with entitlements and credentials', () => {
|
|||||||
assert.equal(uninsuredOrder.travelers.length, 2)
|
assert.equal(uninsuredOrder.travelers.length, 2)
|
||||||
assert.deepEqual(uninsuredOrder.entitlements.map((entitlement) => entitlement.quantity), [2, 2])
|
assert.deepEqual(uninsuredOrder.entitlements.map((entitlement) => entitlement.quantity), [2, 2])
|
||||||
assert.deepEqual(uninsuredOrder.entitlements.map((entitlement) => entitlement.status), ['pending', 'pending'])
|
assert.deepEqual(uninsuredOrder.entitlements.map((entitlement) => entitlement.status), ['pending', 'pending'])
|
||||||
assert.equal(uninsuredOrder.insurance, undefined)
|
assert.equal(uninsuredOrder.insurance, null)
|
||||||
|
|
||||||
const insuredOrder = createPaidOrder({
|
const insuredOrder = createPaidOrder({
|
||||||
product: adultProduct,
|
product: adultProduct,
|
||||||
@@ -201,12 +205,14 @@ test('creates a paid order with entitlements and credentials', () => {
|
|||||||
assert.deepEqual(insuredOrder.travelers.map((traveler) => traveler.id), ['t-1', 't-2'])
|
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.quantity), [2, 2])
|
||||||
assert.deepEqual(insuredOrder.entitlements.map((entitlement) => entitlement.refundAllocation), [120, 40])
|
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.notEqual(insuredOrder.entitlements[0], adultProduct.entitlements[0])
|
||||||
assert.deepEqual(adultProduct.entitlements, [
|
assert.deepEqual(adultProduct.entitlements, [
|
||||||
{ id: 'adult-ticket', name: '成人门票', allocation: 120, refundAllocation: 120, third_party: true },
|
{ id: 'adult-ticket', name: '成人门票', fulfillment: 'third_party', quantity: 1, unit: '张', refundAllocation: 120 },
|
||||||
{ id: 'shuttle', name: '摆渡车', allocation: 40, refundAllocation: 40, self: true },
|
{ id: 'shuttle', name: '观光车票', fulfillment: 'self', quantity: 1, unit: '张', refundAllocation: 40 },
|
||||||
])
|
])
|
||||||
assert.deepEqual(insuredOrder.insurance, { price: 5, amount: 10 })
|
assert.deepEqual(insuredOrder.insurance, { name: '出行意外险', price: 5, quantity: 2, status: 'active' })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('applies refunds and pops view history without mutation', () => {
|
test('applies refunds and pops view history without mutation', () => {
|
||||||
@@ -231,6 +237,7 @@ test('applies refunds and pops view history without mutation', () => {
|
|||||||
assert.equal(result.order.status, 'refunding')
|
assert.equal(result.order.status, 'refunding')
|
||||||
assert.equal(result.refund.amount, 120)
|
assert.equal(result.refund.amount, 120)
|
||||||
assert.equal(result.order.entitlements[0].status, 'refunding')
|
assert.equal(result.order.entitlements[0].status, 'refunding')
|
||||||
|
assert.deepEqual(result.order.logs.at(-1), { time: '2026-07-30 10:01:00', text: '已申请退款', operator: '用户' })
|
||||||
const repeatedRefund = applyRefund(result.order, [entitlementId, 'missing-entitlement'], '重复申请')
|
const repeatedRefund = applyRefund(result.order, [entitlementId, 'missing-entitlement'], '重复申请')
|
||||||
assert.equal(repeatedRefund.refund.amount, 0)
|
assert.equal(repeatedRefund.refund.amount, 0)
|
||||||
assert.deepEqual(repeatedRefund.refund.entitlementIds, [])
|
assert.deepEqual(repeatedRefund.refund.entitlementIds, [])
|
||||||
|
|||||||
Reference in New Issue
Block a user