const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const adapter = require('../erp_wechat_adapter'); function tempAuditDir() { return fs.mkdtempSync(path.join(os.tmpdir(), 'erp-wechat-audit-')); } test('creates a ready task for a valid team batch order', () => { const input = `下单模式:团队-批量下单 订单性质:测试 预订客户:LW云南七彩金桥国旅(昆明景腾) 产品名称:遇见老挝(常规)6D5N 发团日期范围:从:2026-07-13 到:2026-07-15 发团周期:天天 默认人数:成人:2 小孩占床:1 小孩不占床:1 婴儿:0 领队:0 默认用房:SGL:0 TWN:0 TRP:0 DBL:2 HNM:0 TL:0 默认单价:成人:520 小孩占床:220 小孩不占床:120 婴儿:0 领队:0 计调OP:测试 销售人:琳琳 特殊日期调整:2026-07-14:成人 1,小孩占床 1,小孩不占床 1,DBL 1 备注:测试单,ERP 保存后可删除。`; const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.equal(result.route, 'team_batch'); assert.equal(result.task.route, 'team_batch'); assert.equal(result.task.orderNature, '测试'); assert.equal(result.task.fields.bookingCustomer, 'LW云南七彩金桥国旅(昆明景腾)'); assert.equal(result.task.fields.productName, '遇见老挝(常规)6D5N'); assert.deepEqual(result.task.fields.departureDates, ['2026-07-13', '2026-07-14', '2026-07-15']); assert.equal(result.task.fields.defaultPax.adult, 2); assert.equal(result.task.fields.defaultRooms.DBL, 2); assert.equal(result.task.fields.defaultPrices.childNoBed, 120); assert.match(result.customerMessage, /可以进入 ERP 自动下单/); assert.ok(fs.existsSync(result.auditPath)); }); test('creates a team batch task from customer-friendly date list wording', () => { const input = `下单模式:团队-批量下单 订单性质:正式 客户/渠道:LW云南七彩金桥国旅(昆明景腾) 产品名称:遇见老挝(常规)6D5N 出发日期: 2026-08-13 2026-08-14 2026-08-15 人数: 成人:2 小孩占床:1 小孩不占床:1 婴儿:0 领队:0 用房: SGL:0 TWN:0 TRP:0 DBL:2 HNM:0 TL:0 单价: 成人:520 小孩占床:220 小孩不占床:120 婴儿:0 领队:0 计调OP:测试 销售人:琳琳 备注:团队批量重复提交安全测试,先 dry-run,不要真实保存 ERP,不要导出附件。`; const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.equal(result.route, 'team_batch'); assert.equal(result.task.fields.bookingCustomer, 'LW云南七彩金桥国旅(昆明景腾)'); assert.deepEqual(result.task.fields.departureDates, ['2026-08-13', '2026-08-14', '2026-08-15']); assert.equal(result.task.fields.dateRange.start, '2026-08-13'); assert.equal(result.task.fields.dateRange.end, '2026-08-15'); assert.equal(result.task.fields.cycle, '指定日期'); assert.equal(result.task.fields.defaultPax.adult, 2); assert.equal(result.task.fields.defaultRooms.DBL, 2); assert.equal(result.task.fields.defaultPrices.childNoBed, 120); }); test('blocks team single orders that are missing salesperson', () => { const input = `下单模式:团队-单个下单 订单性质:测试 产品名称:遇见老挝 出发日期:2026-07-05 人数:成人:2 小孩占床:2 小孩不占床:2 婴儿:0 领队:0 单价:成人:500 小孩占床:200 小孩不占床:100 计调OP:测试 销售人:`; const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir() }); assert.equal(result.status, 'needs_clarification'); assert.equal(result.route, 'team_single'); assert.deepEqual(result.missingFields, ['销售人']); assert.match(result.customerMessage, /销售人/); assert.equal(result.task, null); }); test('example team single order creates a ready task', () => { const input = fs.readFileSync( path.join(__dirname, '..', '..', 'examples', 'wechat-orders', 'team-single.txt'), 'utf8' ); const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.equal(result.route, 'team_single'); assert.equal(result.task.route, 'team_single'); assert.equal(result.task.fields.productName, '遇见老挝'); assert.equal(result.task.fields.departureDate, '2026-07-12'); assert.equal(result.task.fields.pax.adult, 2); assert.equal(result.task.fields.rooms.DBL, 2); assert.equal(result.task.fields.prices.adult, 500); assert.equal(result.task.fields.op, '测试'); assert.equal(result.task.fields.salesperson, '琳琳'); }); test('team single order preserves submitted booking customer', () => { const input = fs.readFileSync( path.join(__dirname, '..', '..', 'examples', 'wechat-orders', 'team-single.txt'), 'utf8' ).replace( '订单性质:测试', '订单性质:测试\n预订客户:LW云南七彩金桥国旅(昆明景腾)' ); const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.equal(result.route, 'team_single'); assert.equal(result.task.fields.bookingCustomer, 'LW云南七彩金桥国旅(昆明景腾)'); }); test('ready create-order task defaults to deferred confirmation export', () => { const input = fs.readFileSync( path.join(__dirname, '..', '..', 'examples', 'wechat-orders', 'team-single.txt'), 'utf8' ); const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.equal(result.task.operation, 'create_order'); assert.equal(result.task.delivery.deferConfirmationExport, true); assert.equal(result.task.delivery.sendPdfFollowUp, false); }); test('attaches traveler-list files to ready orders when the total matches pax', () => { const input = fs.readFileSync( path.join(__dirname, '..', '..', 'examples', 'wechat-orders', 'team-single.txt'), 'utf8' ); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'erp-traveler-list-')); const csvPath = path.join(tmp, 'travelers.csv'); fs.writeFileSync( csvPath, [ '序号,中文名,英文名,性别,身份证号码,出生日期,出生地,护照号码,签发日期,有效日期,签发地,备注', '1,张三,ZHANG SAN,男,110101199001011234,1990-01-01,北京,E12345678,2025-01-01,2035-01-01,北京,', '2,李四,LI SI,女,110101199201011234,1992-01-01,北京,E23456789,2025-01-01,2035-01-01,北京,', '3,王五,WANG WU,男,110101199301011234,1993-01-01,北京,E34567890,2025-01-01,2035-01-01,北京,', '4,赵六,ZHAO LIU,女,110101199401011234,1994-01-01,北京,E45678901,2025-01-01,2035-01-01,北京,', ].join('\n'), 'utf8' ); const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir(), attachments: [csvPath], }); const audit = JSON.parse(fs.readFileSync(result.auditPath, 'utf8')); assert.equal(result.status, 'ready'); assert.equal(result.task.fields.travelerList.totalCount, 4); assert.equal(result.task.fields.travelerList.validationStatus, 'matched'); assert.equal(audit.task.fields.travelerList.rows[0].chineseName, '[redacted]'); assert.equal(audit.task.fields.travelerList.rows[0].passportNo, '[redacted]'); }); test('blocks traveler-list attachments when total travelers differ from order pax', () => { const input = fs.readFileSync( path.join(__dirname, '..', '..', 'examples', 'wechat-orders', 'team-single.txt'), 'utf8' ); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'erp-traveler-list-')); const csvPath = path.join(tmp, 'travelers.csv'); fs.writeFileSync( csvPath, [ '序号,中文名,英文名,性别', '1,张三,ZHANG SAN,男', '2,李四,LI SI,女', ].join('\n'), 'utf8' ); const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir(), attachments: [csvPath], }); assert.equal(result.status, 'needs_clarification'); assert.equal(result.conflict, 'traveler_count_mismatch'); assert.equal(result.travelerList.totalCount, 2); assert.equal(result.travelerList.expectedTotal, 4); assert.equal(result.task, null); }); test('accepts negative unit prices as filled price fields', () => { const input = fs.readFileSync( path.join(__dirname, '..', '..', 'examples', 'wechat-orders', 'team-single.txt'), 'utf8' ).replace(/(单价:成人:)500/, '$1-500'); const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.equal(result.task.fields.prices.adult, -500); }); test('blocks team batch orders that include multiple products', () => { const input = `下单模式:团队-批量下单 订单性质:测试 预订客户:LW云南七彩金桥国旅(昆明景腾) 产品名称:遇见老挝 6D5N、老挝轻奢 7D6N 发团日期范围:从:2026-07-13 到:2026-07-15 发团周期:天天 默认人数:成人:2 小孩占床:1 小孩不占床:1 婴儿:0 领队:0 默认用房:SGL:0 TWN:0 TRP:0 DBL:2 HNM:0 TL:0 默认单价:成人:520 小孩占床:220 小孩不占床:120 婴儿:0 领队:0 计调OP:测试 销售人:琳琳`; const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir() }); assert.equal(result.status, 'needs_clarification'); assert.equal(result.route, 'team_batch'); assert.equal(result.conflict, 'team_batch_multiple_products'); assert.match(result.customerMessage, /一次只能选择一个产品/); assert.equal(result.task, null); }); test('blocks messages that contain multiple route blocks', () => { const input = `下单模式:团队-单个下单 订单性质:测试 产品名称:遇见老挝 下单模式:散拼-拼单/录入子单 母团号:LW-260701A-F`; const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir() }); assert.equal(result.status, 'needs_clarification'); assert.equal(result.route, null); assert.equal(result.conflict, 'multiple_routes'); assert.match(result.customerMessage, /一次只发一个下单模式/); assert.equal(result.task, null); }); test('creates a split child task without inventing child order identifiers', () => { const input = `下单模式:散拼-拼单/录入子单 订单性质:测试 母团号:LW-260701A-F 出发日期:2026-07-01 渠道/预订客户:LW云南七彩金桥国旅(昆明景腾) 人数:成人:5 小孩占床:0 小孩不占床:0 婴儿:0 领队:0 单价:成人:500 小孩占床:0 小孩不占床:0 婴儿:0 领队:0 计调OP:测试 销售人:琳琳`; const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.equal(result.route, 'split_child'); assert.equal(result.task.fields.parentGroupNo, 'LW-260701A-F'); assert.equal(result.task.fields.channelCustomer, 'LW云南七彩金桥国旅(昆明景腾)'); assert.equal(result.task.fields.pax.adult, 5); assert.deepEqual(result.task.erpIdentifiers, {}); assert.match(result.customerMessage, /可以进入 ERP 自动下单/); }); test('creates a split child task from product route when parent group is omitted', () => { const input = `下单模式:散拼-拼单/录入子单 订单性质:测试 产品/线路:老挝行程-广东 8D7N 出发日期:2026-07-01 渠道/预订客户:LW云南七彩金桥国旅(昆明景腾) 人数:成人:5 小孩占床:0 小孩不占床:0 婴儿:0 领队:0 单价:成人:500 小孩占床:0 小孩不占床:0 婴儿:0 领队:0 计调OP:测试 销售人:琳琳`; const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.equal(result.route, 'split_child'); assert.equal(result.task.fields.parentGroupNo, ''); assert.equal(result.task.fields.productRoute, '老挝行程-广东 8D7N'); assert.equal(result.task.fields.departureDate, '2026-07-01'); assert.equal(result.task.fields.pax.adult, 5); }); test('captures split child supplemental travel details when provided', () => { const input = `下单模式:散拼-拼单/录入子单 订单性质:测试 产品/线路:老挝行程-广东 8D7N 出发日期:2026-09-19 渠道/预订客户:LW云南七彩金桥国旅(昆明景腾) 人数: 成人:2 小孩占床:1 小孩不占床:1 婴儿:0 领队:0 单价: 成人:500 小孩占床:300 小孩不占床:200 婴儿:0 领队:0 计调OP:测试 销售人:琳琳 航班信息: 去程:2026-09-19,CZ6091,广州-万象,08:20-10:10 回程:2026-09-26,CZ6092,万象-广州,18:30-22:10 酒店信息: 第1晚:万象湄公河精品酒店,2间双床房 第2晚:万荣河畔酒店,2间双床房 接送机信息: 接机地点:万象机场到达口 送机地点:万象机场国际出发口 联系人:张三 电话:13800138000 游客名单: 1. 张三 / 男 / 成人 / 护照 E12345678 / 电话 13800138000 2. 李四 / 女 / 成人 / 护照 E23456789 / 电话 13900139000 3. 王小明 / 男 / 小孩占床 / 护照 E34567890 4. 王小小 / 女 / 小孩不占床 / 护照 E45678901 特殊要求: 1. 两间房尽量安排同楼层 2. 小孩不占床,需要额外早餐报价 3. 客人不吃牛肉 4. 确认书需要返回 PDF 备注:录屏完整流程测试2,散拼录入子单,不填写母团号,系统自动匹配 2026-09-19 的母团,保存 ERP,返回子单号和 PDF。`; const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.equal(result.route, 'split_child'); assert.equal(result.task.fields.supplemental.flights.length, 2); assert.deepEqual(result.task.fields.supplemental.flights[0], { label: '去程', date: '2026-09-19', flightNo: 'CZ6091', from: '广州', to: '万象', departTime: '08:20', arriveTime: '10:10', raw: '去程:2026-09-19,CZ6091,广州-万象,08:20-10:10', }); assert.equal(result.task.fields.supplemental.hotels[0], '第1晚:万象湄公河精品酒店,2间双床房'); assert.equal(result.task.fields.supplemental.transfer.pickupPlace, '万象机场到达口'); assert.equal(result.task.fields.supplemental.transfer.dropoffPlace, '万象机场国际出发口'); assert.equal(result.task.fields.supplemental.transfer.contact, '张三'); assert.equal(result.task.fields.supplemental.travelers.length, 4); assert.deepEqual(result.task.fields.supplemental.travelers[0], { name: '张三', gender: '男', type: '成人', passportNo: 'E12345678', phone: '13800138000', raw: '1. 张三 / 男 / 成人 / 护照 E12345678 / 电话 13800138000', }); assert.deepEqual(result.task.fields.supplemental.specialRequests, [ '两间房尽量安排同楼层', '小孩不占床,需要额外早餐报价', '客人不吃牛肉', '确认书需要返回 PDF', ]); }); test('captures colon-delimited supplemental flights from WeChat order text', () => { const input = `下单模式:散拼-拼单/录入子单 订单性质:测试 产品/线路:老挝行程-广东 8D7N 出发日期:2026-07-20 渠道/预订客户:LW云南七彩金桥国旅(昆明景腾) 人数: 成人:2 小孩占床:0 小孩不占床:0 婴儿:0 领队:0 单价: 成人:520 小孩占床:0 小孩不占床:0 婴儿:0 领队:0 计调OP:小七 销售人:琳琳 航班信息: 去程:2026-07-20:CZ6091:广州-万象:08:10-10:15 回程:2026-07-25:CZ6092:万象-广州:11:15-15:20`; const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.deepEqual(result.task.fields.supplemental.flights, [ { label: '去程', date: '2026-07-20', flightNo: 'CZ6091', from: '广州', to: '万象', departTime: '08:10', arriveTime: '10:15', raw: '去程:2026-07-20:CZ6091:广州-万象:08:10-10:15', }, { label: '回程', date: '2026-07-25', flightNo: 'CZ6092', from: '万象', to: '广州', departTime: '11:15', arriveTime: '15:20', raw: '回程:2026-07-25:CZ6092:万象-广州:11:15-15:20', }, ]); }); test('example split child order creates a ready task', () => { const input = fs.readFileSync( path.join(__dirname, '..', '..', 'examples', 'wechat-orders', 'split-child.txt'), 'utf8' ); const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.equal(result.route, 'split_child'); assert.equal(result.task.route, 'split_child'); assert.equal(result.task.fields.parentGroupNo, ''); assert.equal(result.task.fields.productRoute, '老挝行程-广东 8D7N'); assert.equal(result.task.fields.departureDate, '2026-07-01'); assert.equal(result.task.fields.pax.adult, 5); assert.equal(result.task.fields.prices.adult, 500); assert.deepEqual(result.task.erpIdentifiers, {}); }); test('example split parent order creates a ready task', () => { const input = fs.readFileSync( path.join(__dirname, '..', '..', 'examples', 'wechat-orders', 'split-parent.txt'), 'utf8' ); const result = adapter.handleIncomingOrder(input, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.equal(result.route, 'split_parent'); assert.equal(result.task.route, 'split_parent'); assert.equal(result.task.fields.productRoute, '老挝行程-广东 8D7N'); assert.deepEqual(result.task.fields.departureDates, [ '2026-07-04', '2026-07-11', '2026-07-18', '2026-07-25', ]); assert.equal(result.task.fields.plannedGuests, 10); assert.equal(result.task.fields.parentPlansPerDate, 1); assert.equal(result.task.fields.followOp, '测试'); assert.deepEqual(result.task.erpIdentifiers, {}); });