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