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_operation_adapter'); function tempAuditDir() { return fs.mkdtempSync(path.join(os.tmpdir(), 'erp-operation-audit-')); } test('defaults old order templates to create_order and delegates route parsing', () => { const result = adapter.handleIncomingMessage(`下单模式:团队-单个下单 订单性质:测试 产品名称:遇见老挝 出发日期:2026-10-03 人数: 成人:2 小孩占床:0 小孩不占床:0 婴儿:0 领队:0 用房: SGL:0 TWN:0 TRP:0 DBL:1 HNM:0 TL:0 单价: 成人:500 小孩占床:0 小孩不占床:0 婴儿:0 领队:0 计调OP:测试 销售人:琳琳 备注:确认真实保存 ERP`, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.equal(result.operation, 'create_order'); assert.equal(result.task.operation, 'create_order'); assert.equal(result.task.route, 'team_single'); assert.equal(result.task.delivery.deferConfirmationExport, true); assert.equal(result.task.delivery.sendPdfFollowUp, false); }); test('treats order templates with supplemental and pdf wording as create_order', () => { const result = adapter.handleIncomingMessage(`下单模式:散拼-拼单/录入子单 订单性质:测试 产品/线路:老挝行程-广东 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间双床房 接送机信息: 接机地点:万象机场到达口 送机地点:万象机场国际出发口 联系人:张三 电话:13800138000 游客名单: 1. 张三 / 男 / 成人 / 护照 E12345678 / 电话 13800138000 特殊要求: 1. 确认书需要返回 PDF 备注:补充资料包含航班、酒店、游客名单和特殊要求。确认真实保存 ERP。`, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.equal(result.operation, 'create_order'); assert.equal(result.task.operation, 'create_order'); assert.equal(result.task.route, 'split_child'); assert.equal(result.task.fields.supplemental.flights.length, 2); assert.equal(result.task.fields.supplemental.hotels[0], '第1晚:万象湄公河精品酒店,2间双床房'); assert.equal(result.task.fields.supplemental.transfer.pickupPlace, '万象机场到达口'); assert.equal(result.task.fields.supplemental.travelers[0].name, '张三'); assert.deepEqual(result.task.fields.supplemental.specialRequests, ['确认书需要返回 PDF']); }); test('parses update_order by ERP identifier without requiring a route', () => { const result = adapter.handleIncomingMessage(`操作类型:补充/修改订单 订单编号:D14079 修改内容: 航班信息: 去程:2026-09-19,CZ6091,广州-万象,08:20-10:10 特殊要求: 客人不吃牛肉`, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.equal(result.operation, 'update_order'); assert.equal(result.task.operation, 'update_order'); assert.equal(result.task.identifier, 'D14079'); assert.match(result.task.updateText, /CZ6091/); assert.equal(result.task.route, ''); }); test('adds structured update plan for update_order instructions', () => { const result = adapter.handleIncomingMessage(`操作类型:修改订单 订单编号:D14079 修改内容: 成人减少1 DBL减少1 备注增加:客人不吃牛肉`, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.equal(result.operation, 'update_order'); assert.deepEqual(result.task.updatePlan.actions, [ { target: 'pax.adult', operation: 'delta', value: -1, source: '成人减少1' }, { target: 'rooms.DBL', operation: 'delta', value: -1, source: 'DBL减少1' }, { target: 'remark', operation: 'append', value: '客人不吃牛肉', source: '备注增加:客人不吃牛肉' }, ]); }); test('attaches traveler-list files to update_order instructions and redacts audit rows', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'erp-operation-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,北京,', ].join('\n'), 'utf8' ); const result = adapter.handleIncomingMessage(`操作类型:补充/修改订单 订单编号:D14079`, { auditDir: tempAuditDir(), attachments: [csvPath], }); const audit = JSON.parse(fs.readFileSync(result.auditPath, 'utf8')); assert.equal(result.status, 'ready'); assert.equal(result.operation, 'update_order'); assert.equal(result.task.updatePlan.supplemental.travelerList.totalCount, 2); assert.equal(result.task.updatePlan.supplemental.travelerList.validationStatus, 'pending_order_pax'); assert.equal(result.task.updatePlan.actions.at(-1).target, 'supplemental.travelerList'); assert.equal(audit.task.updatePlan.supplemental.travelerList.rows[0].chineseName, '[redacted]'); assert.equal(audit.task.updatePlan.supplemental.travelerList.rows[0].passportNo, '[redacted]'); }); test('treats natural traveler-list import phrase with an identifier as update_order', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'erp-operation-natural-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,鍖椾含,', ].join('\n'), 'utf8' ); const result = adapter.handleIncomingMessage('导入到 LLW-260805A-A', { auditDir: tempAuditDir(), attachments: [csvPath], }); assert.equal(result.status, 'ready'); assert.equal(result.operation, 'update_order'); assert.equal(result.task.identifier, 'LLW-260805A-A'); assert.equal(result.task.updatePlan.supplemental.travelerList.sourceFile, csvPath); assert.equal(result.task.updatePlan.supplemental.travelerList.validationStatus, 'pending_order_pax'); assert.equal(result.task.updatePlan.actions.at(-1).target, 'supplemental.travelerList'); }); test('asks clarification when update_order instructions are ambiguous', () => { const result = adapter.handleIncomingMessage(`操作类型:修改订单 订单编号:D14079 修改内容: 人数减1 房间减少1间`, { auditDir: tempAuditDir() }); assert.equal(result.status, 'needs_clarification'); assert.equal(result.operation, 'update_order'); assert.deepEqual(result.missingFields, ['人数类别', '房型']); assert.equal(result.task, null); assert.match(result.customerMessage, /人数类别/); }); test('parses export_confirmation by ERP identifier', () => { const result = adapter.handleIncomingMessage(`操作类型:导出确认件 订单编号:LLW-260811A-A`, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.equal(result.operation, 'export_confirmation'); assert.equal(result.task.operation, 'export_confirmation'); assert.equal(result.task.identifier, 'LLW-260811A-A'); assert.deepEqual(result.task.exportTypes, ['xingyou-confirm']); }); test('parses explicit Liantai confirmation export request', () => { const result = adapter.handleIncomingMessage(`操作类型:导出确认件 订单编号:LW-260919A-A 需要联泰确认书`, { auditDir: tempAuditDir() }); assert.equal(result.status, 'ready'); assert.deepEqual(result.task.exportTypes, ['xingyou-confirm', 'liantai-confirm']); }); test('blocks update_order when identifier is missing', () => { const result = adapter.handleIncomingMessage(`操作类型:补充/修改订单 修改内容:备注:客人不吃牛肉`, { auditDir: tempAuditDir() }); assert.equal(result.status, 'needs_clarification'); assert.equal(result.operation, 'update_order'); assert.deepEqual(result.missingFields, ['订单编号']); assert.match(result.customerMessage, /订单编号/); }); test('parseArgs supports input audit-dir and json flags', () => { const args = adapter.parseArgs(['node', 'tool', '--input', 'message.txt', '--audit-dir', 'audit', '--json']); assert.equal(args.input, 'message.txt'); assert.equal(args.auditDir, 'audit'); assert.equal(args.json, true); });