Files
LWLT-AIBOT/archive/handoff/2026-07-12/legacy-erp-handoff/tools/tests/erp_wechat_adapter.test.js
T

505 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, {});
});