You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

248 lines
9.8 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { describe, expect, it } from "vitest";
import {
extractBlPlusFromMail,
mapBlModulesToHeader,
parseBlPlusTemplate,
parseSpaceForecastTemplate,
usesPlusFieldDelimiter,
} from "@/services/parse/bl-plus-template";
import {
extractContainerHeaderFromText,
shippingLineHintFromBl,
} from "@/services/parse/extract-container-header";
const SAMPLE =
"新辰泽+WHLC027G597465+WHSU8127240+洛杉矶+40HQ+EDT2026.04-25 ETA2026.05-15船名航次HMM EMERALD 013E+提拆派组合柜-不带托架";
describe("parseBlPlusTemplate", () => {
it("parses standard plus template modules", () => {
const m = parseBlPlusTemplate(SAMPLE);
expect(m).not.toBeNull();
expect(m!.customer_name).toBe("新辰泽");
expect(m!.bl_no).toBe("WHLC027G597465");
expect(m!.shipping_line_hint).toBe("WHLC");
expect(m!.container_no).toBe("WHSU8127240");
expect(m!.port).toBe("洛杉矶");
expect(m!.cabinet_type).toBe("40HQ");
expect(m!.etd).toBe("2026-04-25");
expect(m!.eta).toBe("2026-05-15");
expect(m!.vessel_voyage).toContain("HMM EMERALD");
expect(m!.service).toContain("提拆派");
expect(m!.classis).toBe(0);
expect(m!.ops_hints).toEqual(
expect.arrayContaining(["提柜", "拆柜", "派送"]),
);
});
it("maps modules onto SaveContainer-aligned header fields", () => {
const m = parseBlPlusTemplate(SAMPLE)!;
const header = mapBlModulesToHeader(
{ F_ContainerNo: "", container_no_valid: false },
m,
);
expect(header.F_ContainerNo).toBe("WHSU8127240");
expect(header.F_BLCopyCode).toBe("WHLC027G597465");
expect(header.F_Dock).toBe("洛杉矶");
expect(header.F_CabinetType).toBe("40HQ");
expect(header.F_ETD).toBe("2026-04-25");
expect(header.F_ETA).toBe("2026-05-15");
expect(header.F_Classis).toBe(0);
expect(header.F_OperationType).toBe(0);
expect(header.F_TransMode).toBe(0);
expect(header.F_Instruction).toMatch(/船名航次.*HMM EMERALD/);
expect(header.F_MemoRemark).toBeUndefined();
});
it("extracts from subject + body like template mail", () => {
const r = extractBlPlusFromMail(
`新增预报136:${SAMPLE}`,
`Dear,\n请查收新增预报:${SAMPLE}DO也同步上传至附件请注意查收!`,
);
expect(r).not.toBeNull();
expect(r!.modules.container_no).toBe("WHSU8127240");
expect(r!.modules.bl_no).toBe("WHLC027G597465");
expect(r!.modules.shipping_line_hint).toBe("WHLC");
expect(r!.modules.customer_name).toBe("新辰泽");
});
it("strips Fw/转发/新增预报N from subject customer segment", () => {
const r = extractBlPlusFromMail(
`Fw: 转发:新增预报136:${SAMPLE}`,
"",
);
expect(r).not.toBeNull();
expect(r!.modules.customer_name).toBe("新辰泽");
expect(r!.modules.bl_no).toBe("WHLC027G597465");
expect(r!.modules.shipping_line_hint).toBe("WHLC");
});
it("parses mail2 transfer subject (4 segments)", () => {
const subject =
"Fw: 转发:LINK EVER INC + MATS4583030000+ 柜号:MATU2745683+ ETA : 7/13";
const r = extractBlPlusFromMail(subject, "新增转仓,请留意", {
defaultYear: 2026,
});
expect(r).not.toBeNull();
expect(r!.modules.customer_name).toBe("LINK EVER INC");
expect(r!.modules.bl_no).toBe("MATS4583030000");
expect(r!.modules.container_no).toBe("MATU2745683");
expect(r!.modules.eta).toBe("2026-07-13");
expect(r!.modules.port).toBeUndefined();
expect(r!.modules.vessel_voyage).toBeUndefined();
});
it("parses space-separated ZJYC forecast without inventing port", () => {
const subject = "Fw: Fw:回复:ZJYC COSU6504130784 FCIU7089474 7.10 派拆柜派送";
const body = [
"---- 回复的原邮件 ----",
"主题ZJYC COSU6504130784 FCIU7089474 7.10 派拆柜派送",
"",
"FCIU7089474 提柜 COSCO 40HQ ETA:7.10 卡派拆柜派送",
].join("\n");
const r = extractBlPlusFromMail(subject, body, { defaultYear: 2026 });
expect(r).not.toBeNull();
expect(r!.modules.customer_name).toBe("ZJYC");
expect(r!.modules.bl_no).toBe("COSU6504130784");
expect(r!.modules.container_no).toBe("FCIU7089474");
expect(r!.modules.eta).toBe("2026-07-10");
expect(r!.modules.cabinet_type).toBe("40HQ");
expect(r!.modules.shipping_line_hint).toMatch(/COSCO|COSU/i);
expect(r!.modules.port).toBeUndefined();
const header = mapBlModulesToHeader(
{ F_ContainerNo: "", container_no_valid: false },
r!.modules,
);
expect(header.F_ETA).toBe("2026-07-10");
expect(header.F_Dock).toBe("");
expect(header.F_MemoRemark).toBeUndefined();
});
it("does not treat subjectForParse join noise as destination port", () => {
const subjectForParse =
"Fw: Fw:回复: 回复: LINK EVER INC + ZIMUSHH32195411+ 柜号:ZCSU6581068+ ETA : 7/7 | 回复: 回复: LINK EVER INC + ZIMUSHH32195411+ 柜号:ZCSU6581068+ ETA : 7/7";
const r = extractBlPlusFromMail(subjectForParse, "新增转仓,请留意", {
defaultYear: 2026,
});
expect(r).not.toBeNull();
expect(r!.modules.customer_name).toBe("LINK EVER INC");
expect(r!.modules.container_no).toBe("ZCSU6581068");
expect(r!.modules.bl_no).toBe("ZIMUSHH32195411");
expect(r!.modules.eta).toBe("2026-07-07");
expect(r!.modules.port).toBeUndefined();
const header = mapBlModulesToHeader(
{
F_ContainerNo: "",
container_no_valid: false,
// 模拟库里残留的错误目的港
F_Dock: "ETA : 7/7 | Fw: Fw:回复: 回复: LINK EVER INC",
},
r!.modules,
);
expect(header.F_Dock).toBe("");
expect(header.F_ETA).toBe("2026-07-07");
});
it("parses labeled 柜号/提单号 template without fake customer", () => {
const subject =
"Fw: Fw:回复:柜号:TRHU8524770+提单号:EGLV143661466958+封号:EMCEJJ0635+YWZD+不带车架+整柜直送";
const r = extractBlPlusFromMail(subject, "", { defaultYear: 2026 });
expect(r).not.toBeNull();
expect(r!.modules.container_no).toBe("TRHU8524770");
expect(r!.modules.bl_no).toBe("EGLV143661466958");
// 排除已知字段后仅剩 YWZD → 客户
expect(r!.modules.customer_name).toBe("YWZD");
expect(r!.modules.classis).toBe(0);
expect(r!.modules.ops_hints).toEqual(expect.arrayContaining(["直送"]));
expect(r!.modules.port).toBeUndefined();
const header = mapBlModulesToHeader(
{ F_ContainerNo: "", container_no_valid: false },
r!.modules,
);
expect(header.F_ContainerNo).toBe("TRHU8524770");
expect(header.F_BLCopyCode).toBe("EGLV143661466958");
expect(header.F_Dock).toBe("");
expect(header.F_OperationType).toBe(2);
expect(r!.modules.memo_bits).toEqual(["封号:EMCEJJ0635"]);
expect(header.F_MemoRemark).toBe("封号:EMCEJJ0635");
expect(header.F_MemoRemark).not.toMatch(/柜号|TRHU/);
expect(header.F_MemoRemark).not.toMatch(/YWZD/);
});
it("does not guess customer when multiple leftovers remain", () => {
const subject =
"柜号:TRHU8524770+提单号:EGLV143661466958+封号:EMCEJJ0635+YWZD+ABCDE+不带车架+整柜直送";
const r = extractBlPlusFromMail(subject, "", { defaultYear: 2026 });
expect(r).not.toBeNull();
expect(r!.modules.customer_name).toBeUndefined();
expect(r!.modules.container_no).toBe("TRHU8524770");
expect(r!.modules.bl_no).toBe("EGLV143661466958");
expect(r!.modules.memo_bits).toEqual(
expect.arrayContaining(["封号:EMCEJJ0635", "YWZD", "ABCDE"]),
);
});
it("puts other unmatched labeled fields into memo, not customer", () => {
const subject =
"柜号:TRHU8524770+提单号:EGLV143661466958+预约码:ABC123+YWZD+不带车架+整柜直送";
const r = extractBlPlusFromMail(subject, "", { defaultYear: 2026 });
expect(r!.modules.customer_name).toBe("YWZD");
expect(r!.modules.memo_bits).toEqual(["预约码:ABC123"]);
});
it("plus delimiter and space delimiter are mutually exclusive", () => {
expect(
usesPlusFieldDelimiter(
"新辰泽+WHLC027G597465+WHSU8127240+洛杉矶+40HQ",
),
).toBe(true);
expect(
parseSpaceForecastTemplate(
"新辰泽+WHLC027G597465+WHSU8127240+洛杉矶+40HQ",
{ defaultYear: 2026 },
),
).toBeNull();
expect(
usesPlusFieldDelimiter("ZJYC COSU6504130784 FCIU7089474 7.10 派拆柜派送"),
).toBe(false);
const space = extractBlPlusFromMail(
"ZJYC COSU6504130784 FCIU7089474 7.10 派拆柜派送",
"FCIU7089474 提柜 COSCO 40HQ ETA:7.10 卡派拆柜派送",
{ defaultYear: 2026 },
);
expect(space?.modules.customer_name).toBe("ZJYC");
expect(space?.modules.container_no).toBe("FCIU7089474");
});
it("does not parse ops work-order subject as customer+port forecast", () => {
const subject =
"Fw: Fw:贴标操作:派送指令:拍照指令:do请查收:TCNU4412465+ ONEYNB3BW9236300+船名航次:WAN HAI 511-V.E110+ETA:6/22+UPS-6件+亚马逊卡派-1275件+私人地址-30件+拆柜清单";
const r = extractBlPlusFromMail(subject, "注意:派送要求", {
defaultYear: 2026,
});
expect(r).toBeNull();
});
});
describe("shipping line hint priority", () => {
it("BL prefix WHLC beats vessel HMM", () => {
expect(shippingLineHintFromBl("WHLC027G597465")).toBe("WHLC");
const h = extractContainerHeaderFromText(SAMPLE);
expect(h.shipping_line_hint).toBe("WHLC");
expect(h.F_OperationType).toBe(0);
expect(h.F_TransMode).toBe(0);
expect(h.F_Classis).toBe(0);
});
it("提拆派 wins over 直送 when both present", () => {
const h = extractContainerHeaderFromText("提拆派组合柜,说明:另有直送类型");
expect(h.F_OperationType).toBe(0);
});
it("explicit 船司 still preferred over BL prefix", () => {
const h = extractContainerHeaderFromText(
"船司:MSC\n提单:WHLC027G597465\n柜型:40HQ",
);
expect(h.shipping_line_hint).toBe("MSC");
});
});