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.
363 lines
11 KiB
363 lines
11 KiB
import { describe, expect, it } from "vitest";
|
|
import {
|
|
buildAddressQuery,
|
|
buildDashboardCargo,
|
|
buildDashboardLocation,
|
|
buildPickupDateIso,
|
|
decodeMsCarrierLabel,
|
|
isMsDashboardDirectQuoteEnabled,
|
|
mapDashboardQuoteBodyToItems,
|
|
mapMsDashboardCargoType,
|
|
parseMsReadyTimeToHm,
|
|
zonedWallTimeToUtcIso,
|
|
} from "@/lib/mothership/dashboard-direct-quote";
|
|
import type { QuoteRequest } from "@/modules/providers/quote-provider";
|
|
|
|
describe("dashboard-direct-quote", () => {
|
|
it("decodeMsCarrierLabel 解码 base64 并附加 Direct", () => {
|
|
const label = decodeMsCarrierLabel({
|
|
encodedCarrierName: Buffer.from("ABF Freight", "utf8").toString("base64"),
|
|
serviceLaneType: "direct",
|
|
carrierScac: "abfs",
|
|
});
|
|
expect(label).toBe("ABF Freight Direct");
|
|
});
|
|
|
|
it("mapDashboardQuoteBodyToItems 从 availableRates 产出多承运商", () => {
|
|
const items = mapDashboardQuoteBodyToItems({
|
|
rates: {
|
|
standard: {
|
|
bestValue: {
|
|
id: "rate_a",
|
|
finalPrice: 100,
|
|
days: 3,
|
|
serviceLevel: "standard",
|
|
serviceType: "bestValue",
|
|
carrierScac: "xpol",
|
|
encodedCarrierName: Buffer.from("XPO", "utf8").toString("base64"),
|
|
serviceLaneType: "direct",
|
|
},
|
|
},
|
|
},
|
|
availableRates: {
|
|
rate_a: {
|
|
id: "rate_a",
|
|
finalPrice: 100,
|
|
days: 3,
|
|
serviceLevel: "standard",
|
|
serviceType: "bestValue",
|
|
carrierScac: "xpol",
|
|
encodedCarrierName: Buffer.from("XPO", "utf8").toString("base64"),
|
|
serviceLaneType: "direct",
|
|
},
|
|
rate_b: {
|
|
id: "rate_b",
|
|
finalPrice: 200,
|
|
days: 2,
|
|
serviceLevel: "standard",
|
|
serviceType: "customRate",
|
|
carrierScac: "abfs",
|
|
encodedCarrierName: Buffer.from("ABF", "utf8").toString("base64"),
|
|
serviceLaneType: "direct",
|
|
},
|
|
},
|
|
});
|
|
expect(items.length).toBeGreaterThanOrEqual(2);
|
|
expect(items.some((i) => /XPO/i.test(i.carrier))).toBe(true);
|
|
expect(items.some((i) => /ABF/i.test(i.carrier))).toBe(true);
|
|
expect(items.every((i) => i.rawTotal > 0)).toBe(true);
|
|
});
|
|
|
|
it("buildDashboardLocation 写入二级联系人与营业时间", () => {
|
|
const loc = buildDashboardLocation(
|
|
{
|
|
placeId: "p1",
|
|
city: "Minneapolis",
|
|
state: "MN",
|
|
zip: "55402",
|
|
street: "800 Nicollet Mall",
|
|
timezone: "America/Chicago",
|
|
zone: null,
|
|
coordinates: { latitude: 1, longitude: 2 },
|
|
neighborhood: "",
|
|
},
|
|
["liftgate"],
|
|
{
|
|
company_name: "Acme Co.",
|
|
suite: "Ste 301",
|
|
contact_first: "Ops",
|
|
contact_last: "Lead",
|
|
contact_email: "ops@example.com",
|
|
contact_phone: "5550101",
|
|
opens_at: "8:00 AM",
|
|
closes_at: "5:00 PM",
|
|
},
|
|
);
|
|
expect(loc.accessorials).toEqual(["liftgate"]);
|
|
expect(loc.name).toBe("Acme Co.");
|
|
expect(loc.subStreet).toBe("Ste 301");
|
|
expect(loc.serviceStartTime).toBe("8:00 AM");
|
|
expect(loc.serviceEndTime).toBe("5:00 PM");
|
|
expect(loc.email).toBe("ops@example.com");
|
|
});
|
|
|
|
it("buildAddressQuery 优先结构化地址", () => {
|
|
const q = buildAddressQuery({
|
|
street: "1000 S Alameda St",
|
|
city: "Los Angeles",
|
|
state: "CA",
|
|
zip: "90021",
|
|
placeId: "x",
|
|
formattedAddress: "ignored",
|
|
selectedFromSuggestions: true,
|
|
mothershipOptionId: "x",
|
|
mothershipDisplayLabel: "ignored",
|
|
selectedFromMothership: true,
|
|
});
|
|
expect(q).toContain("1000 S Alameda St");
|
|
expect(q).toContain("Los Angeles");
|
|
});
|
|
|
|
it("buildDashboardCargo 支持多行并向上取整", () => {
|
|
const req = {
|
|
cargoHash: "h",
|
|
pickup: {} as QuoteRequest["pickup"],
|
|
delivery: {} as QuoteRequest["delivery"],
|
|
weightLb: 10.2,
|
|
dimsIn: { l: 48.1, w: 40, h: 48 },
|
|
palletCount: 2,
|
|
cargoType: "general_freight",
|
|
cargoLines: [
|
|
{
|
|
cargoType: "pallet",
|
|
quantity: 2,
|
|
weightLb: 10.2,
|
|
lengthIn: 48.1,
|
|
widthIn: 40,
|
|
heightIn: 48,
|
|
},
|
|
],
|
|
} as QuoteRequest;
|
|
const cargo = buildDashboardCargo(req);
|
|
expect(cargo).toHaveLength(1);
|
|
expect(cargo[0]!.weight).toBe(11);
|
|
expect(cargo[0]!.length).toBe(49);
|
|
expect(cargo[0]!.quantity).toBe(2);
|
|
expect(cargo[0]!.type).toBe("Pallet");
|
|
});
|
|
|
|
it("mapMsDashboardCargoType 与官网货型对齐,禁止非托盘打成 Pallet", () => {
|
|
expect(mapMsDashboardCargoType("case")).toBe("Case");
|
|
expect(mapMsDashboardCargoType("drum")).toBe("Drum");
|
|
expect(mapMsDashboardCargoType("piece")).toBe("Pieces");
|
|
expect(mapMsDashboardCargoType("tote")).toBe("Tote");
|
|
expect(mapMsDashboardCargoType("roll")).toBe("Roll");
|
|
expect(mapMsDashboardCargoType("skid")).toBe("Skid");
|
|
expect(mapMsDashboardCargoType("box")).toBe("Box");
|
|
expect(mapMsDashboardCargoType("carton")).toBe("Carton");
|
|
expect(mapMsDashboardCargoType("crate")).toBe("Crate");
|
|
expect(mapMsDashboardCargoType("pallet")).toBe("Pallet");
|
|
expect(mapMsDashboardCargoType("general_freight")).toBe("Pallet");
|
|
});
|
|
|
|
it("buildDashboardCargo 多货型逐行保留官网 type", () => {
|
|
const req = {
|
|
cargoHash: "h",
|
|
pickup: {} as QuoteRequest["pickup"],
|
|
delivery: {} as QuoteRequest["delivery"],
|
|
weightLb: 500,
|
|
dimsIn: { l: 48, w: 40, h: 48 },
|
|
palletCount: 1,
|
|
cargoType: "general_freight",
|
|
cargoLines: [
|
|
{
|
|
cargoType: "pallet",
|
|
quantity: 2,
|
|
weightLb: 425,
|
|
lengthIn: 42,
|
|
widthIn: 40,
|
|
heightIn: 38,
|
|
},
|
|
{
|
|
cargoType: "drum",
|
|
quantity: 8,
|
|
weightLb: 329,
|
|
lengthIn: 26,
|
|
widthIn: 28,
|
|
heightIn: 19,
|
|
},
|
|
{
|
|
cargoType: "carton",
|
|
quantity: 3,
|
|
weightLb: 166,
|
|
lengthIn: 28,
|
|
widthIn: 19,
|
|
heightIn: 29,
|
|
},
|
|
],
|
|
} as QuoteRequest;
|
|
const cargo = buildDashboardCargo(req);
|
|
expect(cargo.map((c) => c.type)).toEqual(["Pallet", "Drum", "Carton"]);
|
|
});
|
|
|
|
it("buildDashboardCargo piece 行使用官网 API 值 Pieces", () => {
|
|
const req = {
|
|
cargoHash: "h",
|
|
pickup: {} as QuoteRequest["pickup"],
|
|
delivery: {} as QuoteRequest["delivery"],
|
|
weightLb: 255,
|
|
dimsIn: { l: 17, w: 26, h: 24 },
|
|
palletCount: 2,
|
|
cargoType: "general_freight",
|
|
cargoLines: [
|
|
{
|
|
cargoType: "pallet",
|
|
quantity: 2,
|
|
weightLb: 735,
|
|
lengthIn: 48,
|
|
widthIn: 40,
|
|
heightIn: 48,
|
|
},
|
|
{
|
|
cargoType: "piece",
|
|
quantity: 12,
|
|
weightLb: 255,
|
|
lengthIn: 17,
|
|
widthIn: 26,
|
|
heightIn: 24,
|
|
},
|
|
],
|
|
} as QuoteRequest;
|
|
const cargo = buildDashboardCargo(req);
|
|
expect(cargo.map((c) => c.type)).toEqual(["Pallet", "Pieces"]);
|
|
expect(Array.isArray(cargo[1]?.commodities)).toBe(true);
|
|
});
|
|
|
|
it("mapDashboardQuoteBodyToItems 保留同承运商不同价", () => {
|
|
const items = mapDashboardQuoteBodyToItems({
|
|
availableRates: {
|
|
a: {
|
|
id: "a",
|
|
finalPrice: 100,
|
|
days: 5,
|
|
serviceLevel: "standard",
|
|
serviceType: "customRate",
|
|
encodedCarrierName: Buffer.from("ABF Freight", "utf8").toString(
|
|
"base64",
|
|
),
|
|
serviceLaneType: "direct",
|
|
},
|
|
b: {
|
|
id: "b",
|
|
finalPrice: 130,
|
|
days: 3,
|
|
serviceLevel: "standard",
|
|
serviceType: "customRate",
|
|
encodedCarrierName: Buffer.from("ABF Freight", "utf8").toString(
|
|
"base64",
|
|
),
|
|
serviceLaneType: "direct",
|
|
},
|
|
c: {
|
|
id: "c",
|
|
finalPrice: 90,
|
|
days: 6,
|
|
serviceLevel: "standard",
|
|
serviceType: "customRate",
|
|
encodedCarrierName: Buffer.from("XPO", "utf8").toString("base64"),
|
|
serviceLaneType: "direct",
|
|
},
|
|
},
|
|
});
|
|
expect(items.length).toBe(3);
|
|
expect(items.filter((i) => /ABF/i.test(i.carrier))).toHaveLength(2);
|
|
expect(items.filter((i) => /XPO/i.test(i.carrier))).toHaveLength(1);
|
|
});
|
|
|
|
it("mapDashboardQuoteBodyToItems 优先 ratesV2 全量价卡", () => {
|
|
const items = mapDashboardQuoteBodyToItems({
|
|
ratesV2: [
|
|
{
|
|
id: "v2_a",
|
|
finalPrice: 90,
|
|
days: 4,
|
|
serviceLevel: "standard",
|
|
serviceType: "customRate",
|
|
encodedCarrierName: Buffer.from("TForce", "utf8").toString("base64"),
|
|
serviceLaneType: "direct",
|
|
},
|
|
{
|
|
id: "v2_b",
|
|
finalPrice: 110,
|
|
days: 3,
|
|
serviceLevel: "standard",
|
|
serviceType: "customRate",
|
|
encodedCarrierName: Buffer.from("XPO", "utf8").toString("base64"),
|
|
serviceLaneType: "direct",
|
|
},
|
|
],
|
|
availableRates: {
|
|
only_ar: {
|
|
id: "only_ar",
|
|
finalPrice: 999,
|
|
days: 9,
|
|
serviceLevel: "standard",
|
|
serviceType: "customRate",
|
|
encodedCarrierName: Buffer.from("Echo", "utf8").toString("base64"),
|
|
serviceLaneType: "direct",
|
|
},
|
|
},
|
|
});
|
|
expect(items).toHaveLength(2);
|
|
expect(items.some((i) => /Echo/i.test(i.carrier))).toBe(false);
|
|
expect(items.some((i) => /TForce/i.test(i.carrier))).toBe(true);
|
|
});
|
|
|
|
it("buildPickupDateIso 使用提货时区 + 就绪时刻", () => {
|
|
expect(parseMsReadyTimeToHm("8:00 AM")).toEqual({ h: 8, m: 0 });
|
|
expect(parseMsReadyTimeToHm("12:00 PM")).toEqual({ h: 12, m: 0 });
|
|
expect(parseMsReadyTimeToHm("4:00 PM")).toEqual({ h: 16, m: 0 });
|
|
const iso = zonedWallTimeToUtcIso(
|
|
"2026-08-12",
|
|
8,
|
|
0,
|
|
"America/Los_Angeles",
|
|
);
|
|
const asLa = new Intl.DateTimeFormat("en-US", {
|
|
timeZone: "America/Los_Angeles",
|
|
hour: "numeric",
|
|
hourCycle: "h23",
|
|
day: "2-digit",
|
|
}).formatToParts(new Date(iso));
|
|
expect(Number(asLa.find((p) => p.type === "hour")?.value)).toBe(8);
|
|
const req = {
|
|
cargoHash: "h",
|
|
pickup: {} as QuoteRequest["pickup"],
|
|
delivery: {} as QuoteRequest["delivery"],
|
|
weightLb: 500,
|
|
dimsIn: { l: 48, w: 40, h: 48 },
|
|
palletCount: 1,
|
|
cargoType: "pallet",
|
|
readyDate: "2026-08-12",
|
|
readyTime: "4:00 PM",
|
|
} as QuoteRequest;
|
|
const pickupIso = buildPickupDateIso(req, "America/Los_Angeles");
|
|
const hourLa = new Intl.DateTimeFormat("en-US", {
|
|
timeZone: "America/Los_Angeles",
|
|
hour: "2-digit",
|
|
hourCycle: "h23",
|
|
}).formatToParts(new Date(pickupIso));
|
|
expect(Number(hourLa.find((p) => p.type === "hour")?.value)).toBe(16);
|
|
});
|
|
|
|
it("isMsDashboardDirectQuoteEnabled 默认开启", () => {
|
|
const prev = process.env.MS_DASHBOARD_DIRECT_QUOTE;
|
|
delete process.env.MS_DASHBOARD_DIRECT_QUOTE;
|
|
expect(isMsDashboardDirectQuoteEnabled()).toBe(true);
|
|
process.env.MS_DASHBOARD_DIRECT_QUOTE = "false";
|
|
expect(isMsDashboardDirectQuoteEnabled()).toBe(false);
|
|
if (prev === undefined) delete process.env.MS_DASHBOARD_DIRECT_QUOTE;
|
|
else process.env.MS_DASHBOARD_DIRECT_QUOTE = prev;
|
|
});
|
|
});
|