|
|
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");
|
|
|
});
|
|
|
});
|