505 lines
18 KiB
JavaScript
505 lines
18 KiB
JavaScript
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, {});
|
||
});
|