diff --git a/src/pages-service/tickets/data.js b/src/pages-service/tickets/data.js index 2cb0b04..552c612 100644 --- a/src/pages-service/tickets/data.js +++ b/src/pages-service/tickets/data.js @@ -26,15 +26,24 @@ export const GROUPS = [ const TIME_SLOTS = ['08:00—09:30', '09:30—11:30', '11:30—13:30', '13:30—15:00', '15:00—16:00'] const ID_TYPES = ['身份证', '护照', '港澳居民来往内地通行证'] -const createReservation = ({ requiresTimeSlot = true, advanceDays = 0, maxQuantity = 5, timeSlots = TIME_SLOTS } = {}) => ({ - requiresDate: true, +const createReservation = ({ + requiresDate = true, + requiresTimeSlot = true, + realNameRequired = true, + idTypes = ID_TYPES, + contactPhoneRequired = true, + advanceDays = 0, + maxQuantity = 5, + timeSlots = TIME_SLOTS, +} = {}) => ({ + requiresDate, advanceDays, requiresTimeSlot, timeSlots: requiresTimeSlot ? [...timeSlots] : [], - realNameRequired: true, - idTypes: [...ID_TYPES], + realNameRequired, + idTypes: [...idTypes], maxQuantity, - contactPhoneRequired: true, + contactPhoneRequired, }) const createProduct = ({ @@ -81,54 +90,54 @@ const ticketEntitlement = (id, name, fulfillment, refundAllocation) => ({ id, na export const PRODUCTS = [ createProduct({ - id: 'adult', name: '成人票', shortTitle: '成人门票', category: '景点门票', description: '荔波小七孔景区成人入园门票。', salePrice: 160, marketPrice: 170, stock: 120, sold: 2386, - highlights: ['实名预约入园', '覆盖核心景区'], ageRule: { min: 18, max: 59 }, included: ['成人景区门票', '景区观光车'], excluded: ['餐饮及个人消费'], detail: '凭本人有效证件在预约时段入园。', purchaseNotice: '请至少提前一天预约,订单当天不可退改。', + id: 'adult', name: '成人门票+观光车', shortTitle: '成人门票+观光车', category: '景点门票', description: '荔波小七孔景区成人入园门票。', salePrice: 160, marketPrice: 170, stock: 120, sold: 2386, + highlights: ['实名预约入园', '覆盖核心景区'], included: ['成人景区门票', '景区观光车'], excluded: ['餐饮及个人消费'], detail: '凭本人有效证件在预约时段入园。', purchaseNotice: '请至少提前一天预约,订单当天不可退改。', reservation: { advanceDays: 1 }, entitlements: [ticketEntitlement('adult-ticket', '成人门票', 'third_party', 120), ticketEntitlement('shuttle', '观光车票', 'third_party', 40)], }), createProduct({ - id: 'elderly', name: '老年票', shortTitle: '老年优惠票', category: '景点门票', description: '适用于60周岁及以上游客的优惠门票。', salePrice: 100, marketPrice: 160, stock: 80, sold: 816, - highlights: ['60周岁及以上', '实名检票'], ageRule: { min: 60, max: 120 }, included: ['老年优惠门票'], excluded: ['观光车及其他二次消费'], detail: '入园时须出示本人身份证核验年龄。', purchaseNotice: '年龄不符将无法使用,请按实际年龄购票。', - reservation: { advanceDays: 1 }, entitlements: [ticketEntitlement('elderly-ticket', '老年优惠门票', 'third_party', 100)], + id: 'elderly', name: '老年人门票+观光车', shortTitle: '老年人门票+观光车', category: '景点门票', description: '适用于60周岁及以上游客的优惠门票。', salePrice: 100, marketPrice: 160, stock: 80, sold: 816, + highlights: ['60周岁及以上', '实名检票'], ageRule: { min: 60, max: 120 }, included: ['老年优惠门票', '景区观光车'], excluded: ['其他二次消费'], detail: '入园时须出示本人身份证核验年龄。', purchaseNotice: '年龄不符将无法使用,请按实际年龄购票。', + reservation: { advanceDays: 1, idTypes: ['身份证'] }, entitlements: [ticketEntitlement('elderly-ticket', '老年优惠门票', 'third_party', 60), ticketEntitlement('shuttle', '观光车票', 'third_party', 40)], }), createProduct({ - id: 'minor', name: '儿童票', shortTitle: '儿童优惠票', category: '景点门票', description: '适用于6至17周岁未成年人。', salePrice: 100, marketPrice: 160, stock: 75, sold: 629, - highlights: ['6至17周岁适用', '实名预约'], ageRule: { min: 6, max: 17 }, included: ['儿童优惠门票'], excluded: ['观光车及其他二次消费'], detail: '未成年人须使用本人有效身份证件预约。', purchaseNotice: '请按使用日年龄选择票种。', - reservation: { advanceDays: 1 }, entitlements: [ticketEntitlement('minor-ticket', '儿童优惠门票', 'third_party', 100)], + id: 'minor', name: '未成年门票+观光车', shortTitle: '未成年门票+观光车', category: '景点门票', description: '适用于6至17周岁未成年人。', salePrice: 100, marketPrice: 160, stock: 75, sold: 629, + highlights: ['6至17周岁适用', '实名预约'], ageRule: { min: 6, max: 17 }, included: ['未成年优惠门票', '景区观光车'], excluded: ['其他二次消费'], detail: '未成年人须使用本人有效身份证件预约。', purchaseNotice: '请按使用日年龄选择票种。', + reservation: { advanceDays: 1, idTypes: ['身份证'] }, entitlements: [ticketEntitlement('minor-ticket', '儿童优惠门票', 'third_party', 60), ticketEntitlement('shuttle', '观光车票', 'third_party', 40)], }), createProduct({ - id: 'free', name: '优待票', shortTitle: '优待入园票', category: '景点门票', description: '适用于符合景区优待政策的游客。', salePrice: 40, marketPrice: 80, stock: 60, sold: 346, - highlights: ['优待人群适用', '凭证核验'], included: ['优待入园资格'], excluded: ['观光车及其他二次消费'], detail: '入园时须携带可证明优待资格的有效证件。', purchaseNotice: '不符合优待政策者需现场补差价。', - reservation: { advanceDays: 1 }, entitlements: [ticketEntitlement('preferential-ticket', '优待入园票', 'third_party', 40)], + id: 'free', name: '免门票+观光车实名预约', shortTitle: '免门票+观光车实名预约', category: '景点门票', description: '适用于符合景区优待政策的游客。', salePrice: 40, marketPrice: 80, stock: 60, sold: 346, + highlights: ['优待人群适用', '凭证核验'], included: ['免门票', '景区观光车'], excluded: ['其他二次消费'], detail: '入园时须携带可证明优待资格的有效证件。', purchaseNotice: '不符合优待政策者需现场补差价。', + reservation: { advanceDays: 1 }, entitlements: [ticketEntitlement('free-ticket', '免门票', 'third_party', 0), ticketEntitlement('shuttle', '观光车票', 'third_party', 40)], }), createProduct({ id: 'vip-fast', name: 'VIP速通票', shortTitle: 'VIP快速通行', category: 'VIP速通', description: '专属通道快速入园,节省排队时间。', salePrice: 98, marketPrice: 128, stock: 50, sold: 126, highlights: ['专属快速通道', '即买即用'], included: ['VIP速通服务'], excluded: ['景区基础门票'], detail: '需与有效景区门票配合使用。', purchaseNotice: '服务一经核销不可退款。', - reservation: { advanceDays: 0, timeSlots: ['08:00—10:00', '10:00—12:00', '12:00—14:00', '14:00—16:00'] }, entitlements: [ticketEntitlement('vip-fast-pass', 'VIP速通服务', 'self', 98)], + reservation: { advanceDays: 0, realNameRequired: false, timeSlots: ['08:00—10:00', '10:00—12:00', '12:00—14:00', '14:00—16:00'] }, entitlements: [ticketEntitlement('vip-fast-pass', 'VIP速通服务', 'self', 98)], }), createProduct({ - id: 'direct-bus', name: '景区直通车', shortTitle: '往返直通车', category: '直通车', description: '市区往返景区的便捷直通车服务。', salePrice: 68, marketPrice: 88, stock: 40, sold: 92, - highlights: ['往返直达', '固定班次'], included: ['景区直通车单程服务'], excluded: ['景区门票'], detail: '请按预约班次提前15分钟候车。', purchaseNotice: '发车前2小时可免费改期一次。', + id: 'direct-bus', name: '贵阳往返小七孔直通车', shortTitle: '贵阳往返小七孔直通车', category: '直通车', description: '贵阳往返小七孔景区的便捷直通车服务。', salePrice: 68, marketPrice: 88, stock: 40, sold: 92, + highlights: ['往返直达', '固定班次'], included: ['贵阳往返小七孔直通车服务'], excluded: ['景区门票'], detail: '本权益包含固定去程和返程两段车次,构成一次完整往返行程;请按预约班次提前15分钟候车。', purchaseNotice: '06:30—07:00 为固定去程、18:00—18:30 为固定返程,两段均属于同一往返权益,不可按单程理解;发车前2小时可免费改期一次。', reservation: { advanceDays: 0, timeSlots: ['06:30—07:00 去程', '18:00—18:30 返程'] }, entitlements: [ticketEntitlement('direct-bus-seat', '景区直通车', 'third_party', 68)], }), createProduct({ - id: 'classic', name: '经典联票', shortTitle: '门票+观光车', category: '门票+观光车', description: '景区成人门票与观光车组合套餐。', salePrice: 188, marketPrice: 210, stock: 90, sold: 184, - highlights: ['门票观光车联票', '比单买更优惠'], ageRule: { min: 18, max: 59 }, included: ['成人景区门票', '景区观光车', '竹筏体验'], excluded: ['餐饮及个人消费'], detail: '一次预约即可享受门票、观光车与竹筏体验。', purchaseNotice: '各权益可分别核销,未使用部分可按规则申请退款。', - reservation: { advanceDays: 1 }, entitlements: [ticketEntitlement('classic-ticket-shuttle', '成人门票+观光车', 'third_party', 160), ticketEntitlement('classic-boat', '竹筏体验', 'self', 28)], + id: 'classic', name: '门票+观光车+鸳鸯湖游船', shortTitle: '门票+观光车+鸳鸯湖游船', category: '门票+观光车', description: '景区成人门票与观光车组合套餐。', salePrice: 188, marketPrice: 210, stock: 90, sold: 184, + highlights: ['门票观光车联票', '比单买更优惠'], ageRule: { min: 18, max: 59 }, included: ['成人景区门票', '景区观光车', '鸳鸯湖游船'], excluded: ['餐饮及个人消费'], detail: '一次预约即可享受门票、观光车与鸳鸯湖游船。', purchaseNotice: '各权益可分别核销,未使用部分可按规则申请退款。', + reservation: { advanceDays: 1, idTypes: ['身份证'] }, entitlements: [ticketEntitlement('classic-ticket-shuttle', '成人门票+观光车', 'third_party', 160), ticketEntitlement('classic-boat', '鸳鸯湖游船', 'self', 28)], }), createProduct({ - id: 'day-tour', name: '小七孔一日游', shortTitle: '精品一日游', category: '一日游', description: '包含行程组织服务的小七孔一日游。', salePrice: 188, marketPrice: 228, stock: 30, sold: 68, + id: 'day-tour', name: '荔波小七孔精品一日游', shortTitle: '荔波小七孔精品一日游', category: '一日游', description: '包含行程组织服务的小七孔一日游。', salePrice: 188, marketPrice: 228, stock: 30, sold: 68, highlights: ['行程服务', '省心游玩'], included: ['一日游行程服务', '行程讲解'], excluded: ['景区门票及个人消费'], detail: '集合地点与行程安排将于出行前发送。', purchaseNotice: '请在出行前一天18:00前完成预约。', reservation: { advanceDays: 1, maxQuantity: 10, timeSlots: ['08:00—08:30 集合', '09:00—09:30 集合'] }, entitlements: [ticketEntitlement('day-tour-service', '一日游服务', 'self', 188)], }), createProduct({ - id: 'boat', name: '卧龙潭竹筏体验', shortTitle: '竹筏体验', category: '玩乐体验', description: '卧龙潭水域竹筏游览体验。', salePrice: 50, marketPrice: 60, stock: 45, sold: 326, - highlights: ['山水竹筏', '随到随玩'], included: ['竹筏体验一次'], excluded: ['景区门票'], detail: '请在营业时间内前往卧龙潭竹筏码头核销。', purchaseNotice: '受天气影响时,景区可调整运营安排。', - reservation: { requiresTimeSlot: false, advanceDays: 0 }, entitlements: [ticketEntitlement('boat-experience', '卧龙潭竹筏体验', 'self', 50)], + id: 'boat', name: '鸳鸯湖游船票', shortTitle: '鸳鸯湖游船票', category: '玩乐体验', description: '鸳鸯湖水域游船体验。', salePrice: 50, marketPrice: 60, stock: 45, sold: 326, + highlights: ['湖上游船', '随到随玩'], included: ['鸳鸯湖游船一次'], excluded: ['景区门票'], detail: '请在营业时间内前往鸳鸯湖游船码头核销。', purchaseNotice: '受天气影响时,景区可调整运营安排。', + reservation: { requiresTimeSlot: false, realNameRequired: false, advanceDays: 0 }, entitlements: [ticketEntitlement('boat-experience', '鸳鸯湖游船票', 'self', 50)], }), createProduct({ - id: 'guide', name: '人工讲解服务', shortTitle: '专业人工讲解', category: '玩乐体验', description: '专业讲解员陪同讲解景区自然与人文风光。', salePrice: 68, marketPrice: 88, stock: 20, sold: 40, + id: 'guide', name: '小七孔精品讲解服务', shortTitle: '小七孔精品讲解服务', category: '玩乐体验', description: '专业讲解员陪同讲解景区自然与人文风光。', salePrice: 68, marketPrice: 88, stock: 20, sold: 40, highlights: ['专业讲解员', '沉浸式游览'], included: ['60分钟人工讲解服务'], excluded: ['景区门票及交通'], detail: '讲解员将按预约时段在景区入口等候。', purchaseNotice: '请提前10分钟到达指定集合点。', - reservation: { advanceDays: 1, maxQuantity: 10, timeSlots: ['09:00—10:30', '13:30—15:00'] }, entitlements: [ticketEntitlement('guide-service', '人工讲解服务', 'self', 68)], + reservation: { advanceDays: 1, realNameRequired: false, maxQuantity: 10, timeSlots: ['09:00—10:30', '13:30—15:00'] }, entitlements: [ticketEntitlement('guide-service', '人工讲解服务', 'self', 68)], }), ] @@ -146,7 +155,7 @@ export const REFUND_REASONS = ['行程有变,无法按计划出行', '重复 export const INITIAL_TRAVELERS = [ { id: 'traveler-lin-xiao', name: '林晓', idType: '身份证', idNumber: '110101199001020010', phone: '13800138000' }, - { id: 'traveler-zhou-ning', name: '周宁', idType: '身份证', idNumber: '110101196001020010', phone: '13900139000' }, + { id: 'traveler-zhou-ning', name: '周宁', idType: '身份证', idNumber: '110101196001020017', phone: '13900139000' }, ] const createDemoOrder = (productId, draft, now) => createPaidOrder({ diff --git a/src/pages-service/tickets/model.mjs b/src/pages-service/tickets/model.mjs index 72f398e..de2f145 100644 --- a/src/pages-service/tickets/model.mjs +++ b/src/pages-service/tickets/model.mjs @@ -90,22 +90,30 @@ export const createBookingDraft = (product) => ({ }) export const validateBooking = ({ product, draft, travelers }) => { - if (!draft?.visitDate) return '请选择使用日期' - if (!draft.timeSlot) return '请选择使用场次' + const reservation = product?.reservation || {} + const requiresDate = reservation.requiresDate !== false + const requiresTimeSlot = reservation.requiresTimeSlot !== false + const realNameRequired = reservation.realNameRequired !== false + const contactPhoneRequired = reservation.contactPhoneRequired !== false + + if (requiresDate && !draft?.visitDate) return '请选择使用日期' + if (requiresTimeSlot && !draft.timeSlot) 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)) - .filter(Boolean) + if (realNameRequired) { + const selectedTravelers = [...new Set(draft.travelerIds || [])] + .map((id) => travelers.find((traveler) => traveler.id === id)) + .filter(Boolean) - if (selectedTravelers.length < draft.quantity) return '请选择足够数量的出行人' + if (selectedTravelers.length < draft.quantity) return '请选择足够数量的出行人' - for (const traveler of selectedTravelers.slice(0, draft.quantity)) { - if (!traveler.name || !traveler.idType || !traveler.idNumber) return '请完善出行人实名信息' - if (!isTravelerEligible(traveler, product, draft.visitDate)) return '出行人年龄不符合票种要求' + for (const traveler of selectedTravelers.slice(0, draft.quantity)) { + if (!traveler.name || !traveler.idType || !traveler.idNumber) return '请完善出行人实名信息' + if (!isTravelerEligible(traveler, product, draft.visitDate)) return '出行人年龄不符合票种要求' + } } - if (!draft.contactName?.trim() || !/^1[3-9]\d{9}$/.test(draft.phone || '')) return '请填写正确的联系人和手机号' + if (contactPhoneRequired && (!draft.contactName?.trim() || !/^1[3-9]\d{9}$/.test(draft.phone || ''))) return '请填写正确的联系人和手机号' return '' } diff --git a/src/pages-service/tickets/model.test.mjs b/src/pages-service/tickets/model.test.mjs index 4275e1a..073892d 100644 --- a/src/pages-service/tickets/model.test.mjs +++ b/src/pages-service/tickets/model.test.mjs @@ -62,13 +62,13 @@ test('provides coherent default visitor ticket data', async () => { 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: '110101196001020010', phone: '13900139000' }, + { 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'), { - id: 'adult', name: '成人票', shortTitle: '成人门票', category: '景点门票', description: '荔波小七孔景区成人入园门票。', salePrice: 160, marketPrice: 170, stock: 120, sold: 2386, status: 'on_sale', highlights: ['实名预约入园', '覆盖核心景区'], ageRule: { min: 18, max: 59 }, included: ['成人景区门票', '景区观光车'], excluded: ['餐饮及个人消费'], detail: '凭本人有效证件在预约时段入园。', purchaseNotice: '请至少提前一天预约,订单当天不可退改。', images: [PLACEHOLDER_IMAGE], reservation: { requiresDate: true, advanceDays: 1, requiresTimeSlot: true, timeSlots: standardSlots, realNameRequired: true, idTypes: ['身份证', '护照', '港澳居民来往内地通行证'], maxQuantity: 5, contactPhoneRequired: true }, entitlements: [{ id: 'adult-ticket', name: '成人门票', fulfillment: 'third_party', quantity: 1, unit: '张', refundAllocation: 120 }, { id: 'shuttle', name: '观光车票', fulfillment: 'third_party', quantity: 1, unit: '张', refundAllocation: 40 }], + id: 'adult', name: '成人门票+观光车', shortTitle: '成人门票+观光车', category: '景点门票', description: '荔波小七孔景区成人入园门票。', salePrice: 160, marketPrice: 170, stock: 120, sold: 2386, status: 'on_sale', highlights: ['实名预约入园', '覆盖核心景区'], ageRule: null, included: ['成人景区门票', '景区观光车'], excluded: ['餐饮及个人消费'], detail: '凭本人有效证件在预约时段入园。', purchaseNotice: '请至少提前一天预约,订单当天不可退改。', images: [PLACEHOLDER_IMAGE], reservation: { requiresDate: true, advanceDays: 1, requiresTimeSlot: true, timeSlots: standardSlots, realNameRequired: true, idTypes: ['身份证', '护照', '港澳居民来往内地通行证'], maxQuantity: 5, contactPhoneRequired: true }, entitlements: [{ id: 'adult-ticket', name: '成人门票', fulfillment: 'third_party', quantity: 1, unit: '张', refundAllocation: 120 }, { id: 'shuttle', name: '观光车票', fulfillment: 'third_party', quantity: 1, unit: '张', refundAllocation: 40 }], }) assert.deepEqual(productsById.get('classic'), { - id: 'classic', name: '经典联票', shortTitle: '门票+观光车', category: '门票+观光车', description: '景区成人门票与观光车组合套餐。', salePrice: 188, marketPrice: 210, stock: 90, sold: 184, status: 'on_sale', highlights: ['门票观光车联票', '比单买更优惠'], ageRule: { min: 18, max: 59 }, included: ['成人景区门票', '景区观光车', '竹筏体验'], excluded: ['餐饮及个人消费'], detail: '一次预约即可享受门票、观光车与竹筏体验。', purchaseNotice: '各权益可分别核销,未使用部分可按规则申请退款。', images: [PLACEHOLDER_IMAGE], reservation: { requiresDate: true, advanceDays: 1, requiresTimeSlot: true, timeSlots: standardSlots, realNameRequired: true, idTypes: ['身份证', '护照', '港澳居民来往内地通行证'], maxQuantity: 5, contactPhoneRequired: true }, entitlements: [{ id: 'classic-ticket-shuttle', name: '成人门票+观光车', fulfillment: 'third_party', quantity: 1, unit: '张', refundAllocation: 160 }, { id: 'classic-boat', name: '竹筏体验', fulfillment: 'self', quantity: 1, unit: '张', refundAllocation: 28 }], + id: 'classic', name: '门票+观光车+鸳鸯湖游船', shortTitle: '门票+观光车+鸳鸯湖游船', category: '门票+观光车', description: '景区成人门票与观光车组合套餐。', salePrice: 188, marketPrice: 210, stock: 90, sold: 184, status: 'on_sale', highlights: ['门票观光车联票', '比单买更优惠'], ageRule: { min: 18, max: 59 }, included: ['成人景区门票', '景区观光车', '鸳鸯湖游船'], excluded: ['餐饮及个人消费'], detail: '一次预约即可享受门票、观光车与鸳鸯湖游船。', purchaseNotice: '各权益可分别核销,未使用部分可按规则申请退款。', images: [PLACEHOLDER_IMAGE], reservation: { requiresDate: true, advanceDays: 1, requiresTimeSlot: true, timeSlots: standardSlots, realNameRequired: true, idTypes: ['身份证'], maxQuantity: 5, contactPhoneRequired: true }, entitlements: [{ id: 'classic-ticket-shuttle', name: '成人门票+观光车', fulfillment: 'third_party', quantity: 1, unit: '张', refundAllocation: 160 }, { id: 'classic-boat', name: '鸳鸯湖游船', fulfillment: 'self', quantity: 1, unit: '张', refundAllocation: 28 }], }) assert.deepEqual(INITIAL_TRAVELERS[0], { id: 'traveler-lin-xiao', name: '林晓', idType: '身份证', idNumber: '110101199001020010', phone: '13800138000' }) assert.equal(INITIAL_ORDERS.length, 2) @@ -83,6 +83,65 @@ test('provides coherent default visitor ticket data', async () => { } }) +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, /往返/) + 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', 160], ['鸳鸯湖游船', '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: '成人票',