Files
2026-07-13 19:57:46 +08:00

505 lines
18 KiB
JavaScript
Raw Permalink 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
默认用房SGL0 TWN0 TRP0 DBL2 HNM0 TL0
默认单价成人520 小孩占床220 小孩不占床120 婴儿0 领队0
计调OP测试
销售人:琳琳
特殊日期调整2026-07-14成人 1小孩占床 1小孩不占床 1DBL 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
用房:
SGL0
TWN0
TRP0
DBL2
HNM0
TL0
单价:
成人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
默认用房SGL0 TWN0 TRP0 DBL2 HNM0 TL0
默认单价成人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-19CZ6091广州-万象08:20-10:10
回程2026-09-26CZ6092万象-广州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-19CZ6091广州-万象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-20CZ6091广州-万象08:10-10:15
回程2026-07-25CZ6092万象-广州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-20CZ6091广州-万象08:10-10:15',
},
{
label: '回程',
date: '2026-07-25',
flightNo: 'CZ6092',
from: '万象',
to: '广州',
departTime: '11:15',
arriveTime: '15:20',
raw: '回程2026-07-25CZ6092万象-广州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, {});
});