|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
import {
|
|
|
MS_REFINE_DECISION_MS,
|
|
|
MS_REFINE_MSG,
|
|
|
MS_REFINE_TOTAL_MS,
|
|
|
buildMsRefineHoldState,
|
|
|
formatMsRefineRemainingLabel,
|
|
|
isMsRefineHoldExpired,
|
|
|
shouldAutoDeclineRefineHold,
|
|
|
toMsRefineHoldPublic,
|
|
|
} from "@/lib/constants/ms-refine-hold";
|
|
|
import {
|
|
|
collectMsDetailsRefineInvalidFields,
|
|
|
collectMsDetailsRefineMissing,
|
|
|
snapMsReadyTimeToOpensAt,
|
|
|
validateMsDetailsForRefine,
|
|
|
} from "@/lib/mothership/ms-details-refine-validation";
|
|
|
import type { MothershipLoggedInDetailsPayload } from "@/components/mothership/mothership-logged-in-details-form";
|
|
|
|
|
|
const SESSION = "00000000-0000-4000-8000-000000000001";
|
|
|
|
|
|
function fullDetails(
|
|
|
patch?: Partial<MothershipLoggedInDetailsPayload>,
|
|
|
): MothershipLoggedInDetailsPayload {
|
|
|
return {
|
|
|
pickup: {
|
|
|
companyName: "Acme Pickup",
|
|
|
suite: "",
|
|
|
contactFirst: "A",
|
|
|
contactLast: "B",
|
|
|
contactEmail: "pickup@acme.com",
|
|
|
contactPhone: "555-1111",
|
|
|
reference: "",
|
|
|
notes: "",
|
|
|
opensAt: "8:00 AM",
|
|
|
closesAt: "5:00 PM",
|
|
|
},
|
|
|
delivery: {
|
|
|
companyName: "Acme Delivery",
|
|
|
suite: "",
|
|
|
contactFirst: "C",
|
|
|
contactLast: "D",
|
|
|
contactEmail: "delivery@acme.com",
|
|
|
contactPhone: "555-2222",
|
|
|
reference: "",
|
|
|
notes: "",
|
|
|
opensAt: "9:00 AM",
|
|
|
closesAt: "5:00 PM",
|
|
|
},
|
|
|
requestDeliveryAppointment: false,
|
|
|
fbaNumber: "",
|
|
|
fbaPoNumber: "",
|
|
|
cargo: [
|
|
|
{
|
|
|
pieceCountType: "Cartons",
|
|
|
pieceCountQty: 2,
|
|
|
description: "General freight",
|
|
|
nmfc: "",
|
|
|
hazmat: false,
|
|
|
alcohol: false,
|
|
|
tobacco: false,
|
|
|
},
|
|
|
],
|
|
|
...patch,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
describe("ms refine hold — 无客户时限逼迫", () => {
|
|
|
it("构建决策窗与总会话窗(默认 1 小时)", () => {
|
|
|
const now = 1_700_000_000_000;
|
|
|
const s = buildMsRefineHoldState({
|
|
|
quoteId: "q1",
|
|
|
sessionId: SESSION,
|
|
|
customerId: "CUST_001",
|
|
|
nowMs: now,
|
|
|
});
|
|
|
expect(s.decision_deadline_ms).toBe(now + MS_REFINE_DECISION_MS);
|
|
|
expect(s.total_deadline_ms).toBe(now + MS_REFINE_TOTAL_MS);
|
|
|
expect(MS_REFINE_TOTAL_MS).toBe(3_600_000);
|
|
|
});
|
|
|
|
|
|
it("不再因决策窗自动放弃", () => {
|
|
|
const now = 1_000;
|
|
|
const s = buildMsRefineHoldState({
|
|
|
quoteId: "q1",
|
|
|
sessionId: SESSION,
|
|
|
customerId: "CUST_001",
|
|
|
nowMs: now,
|
|
|
});
|
|
|
expect(shouldAutoDeclineRefineHold(s, now + MS_REFINE_DECISION_MS - 1)).toBe(
|
|
|
false,
|
|
|
);
|
|
|
expect(shouldAutoDeclineRefineHold(s, now + MS_REFINE_DECISION_MS)).toBe(
|
|
|
false,
|
|
|
);
|
|
|
expect(
|
|
|
shouldAutoDeclineRefineHold(
|
|
|
{ ...s, status: "filling" },
|
|
|
now + MS_REFINE_DECISION_MS + 10_000,
|
|
|
),
|
|
|
).toBe(false);
|
|
|
});
|
|
|
|
|
|
it("总会话窗边界(内部资源回收)", () => {
|
|
|
const now = 1_000;
|
|
|
const s = buildMsRefineHoldState({
|
|
|
quoteId: "q1",
|
|
|
sessionId: SESSION,
|
|
|
customerId: "CUST_001",
|
|
|
nowMs: now,
|
|
|
});
|
|
|
expect(isMsRefineHoldExpired(s, now + MS_REFINE_TOTAL_MS - 1)).toBe(false);
|
|
|
expect(isMsRefineHoldExpired(s, now + MS_REFINE_TOTAL_MS)).toBe(true);
|
|
|
});
|
|
|
|
|
|
it("倒计时文案:秒 / 分秒 / 归零", () => {
|
|
|
const now = 10_000;
|
|
|
expect(formatMsRefineRemainingLabel(now + 45_000, now)).toBe("45秒");
|
|
|
expect(formatMsRefineRemainingLabel(now + 65_000, now)).toBe("1分05秒");
|
|
|
expect(formatMsRefineRemainingLabel(now - 1, now)).toBe("0秒");
|
|
|
});
|
|
|
|
|
|
it("客户可见文案不含超时/二级/官网敏感词", () => {
|
|
|
expect(MS_REFINE_MSG.decisionTimeout).toBe("请补充必要信息后重新获取报价");
|
|
|
expect(MS_REFINE_MSG.totalTimeout).toBe("请补充必要信息后重新获取报价");
|
|
|
expect(MS_REFINE_MSG.totalTimeoutApi).not.toMatch(/超时|二级|5 分钟/);
|
|
|
expect(MS_REFINE_MSG.decisionTimeoutApi).not.toMatch(/超时|二级/);
|
|
|
});
|
|
|
|
|
|
it("public.available:filling/refining 始终 true;awaiting 在总会话内可用", () => {
|
|
|
const now = 5_000;
|
|
|
const s = buildMsRefineHoldState({
|
|
|
quoteId: "q1",
|
|
|
sessionId: SESSION,
|
|
|
customerId: "CUST_001",
|
|
|
nowMs: now,
|
|
|
});
|
|
|
expect(toMsRefineHoldPublic(s, now + 1_000).available).toBe(true);
|
|
|
expect(
|
|
|
toMsRefineHoldPublic(s, now + MS_REFINE_DECISION_MS + 1).available,
|
|
|
).toBe(false);
|
|
|
const filling = { ...s, status: "filling" as const };
|
|
|
expect(
|
|
|
toMsRefineHoldPublic(filling, now + MS_REFINE_DECISION_MS + 1).available,
|
|
|
).toBe(true);
|
|
|
expect(
|
|
|
toMsRefineHoldPublic(filling, now + MS_REFINE_TOTAL_MS).available,
|
|
|
).toBe(true);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("ms details refine — 必填预防", () => {
|
|
|
it("完整填写通过", () => {
|
|
|
expect(validateMsDetailsForRefine(fullDetails()).ok).toBe(true);
|
|
|
});
|
|
|
|
|
|
it("空公司名/邮箱/电话/营业时间 → 列出缺失项", () => {
|
|
|
const missing = collectMsDetailsRefineMissing(
|
|
|
fullDetails({
|
|
|
pickup: {
|
|
|
...fullDetails().pickup,
|
|
|
companyName: " ",
|
|
|
contactEmail: "",
|
|
|
contactPhone: "",
|
|
|
opensAt: "",
|
|
|
closesAt: "",
|
|
|
},
|
|
|
}),
|
|
|
);
|
|
|
expect(missing.some((m) => m.includes("提货") && m.includes("公司名"))).toBe(
|
|
|
true,
|
|
|
);
|
|
|
expect(missing.some((m) => m.includes("邮箱"))).toBe(true);
|
|
|
expect(missing.some((m) => m.includes("电话"))).toBe(true);
|
|
|
expect(missing.some((m) => m.includes("开门"))).toBe(true);
|
|
|
});
|
|
|
|
|
|
it("邮箱格式无效 → 明确提示", () => {
|
|
|
const missing = collectMsDetailsRefineMissing(
|
|
|
fullDetails({
|
|
|
delivery: { ...fullDetails().delivery, contactEmail: "not-an-email" },
|
|
|
}),
|
|
|
);
|
|
|
expect(missing.some((m) => m.includes("邮箱格式无效"))).toBe(true);
|
|
|
});
|
|
|
|
|
|
it("无货描 → 拦截;件数类型/数量为空不拦截", () => {
|
|
|
const r = validateMsDetailsForRefine(
|
|
|
fullDetails({
|
|
|
cargo: [
|
|
|
{
|
|
|
pieceCountType: "",
|
|
|
pieceCountQty: 0,
|
|
|
description: "",
|
|
|
nmfc: "",
|
|
|
hazmat: false,
|
|
|
alcohol: false,
|
|
|
tobacco: false,
|
|
|
},
|
|
|
],
|
|
|
}),
|
|
|
);
|
|
|
expect(r.ok).toBe(false);
|
|
|
if (!r.ok) {
|
|
|
expect(r.message).toMatch(/信息不完整/);
|
|
|
expect(r.missing.some((m) => m.includes("货物描述"))).toBe(true);
|
|
|
expect(r.missing.some((m) => m.includes("件数类型"))).toBe(false);
|
|
|
expect(r.missing.some((m) => m.includes("件数数量"))).toBe(false);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
it("有货描、无件数类型 → 通过(官网件数类型非必填)", () => {
|
|
|
expect(
|
|
|
validateMsDetailsForRefine(
|
|
|
fullDetails({
|
|
|
cargo: [
|
|
|
{
|
|
|
pieceCountType: "",
|
|
|
pieceCountQty: 0,
|
|
|
description: "Rugs",
|
|
|
nmfc: "",
|
|
|
hazmat: false,
|
|
|
alcohol: false,
|
|
|
tobacco: false,
|
|
|
},
|
|
|
],
|
|
|
}),
|
|
|
).ok,
|
|
|
).toBe(true);
|
|
|
});
|
|
|
|
|
|
it("地址空白 → 拦截", () => {
|
|
|
const missing = collectMsDetailsRefineMissing(fullDetails(), {
|
|
|
pickupAddress: " ",
|
|
|
deliveryAddress: "789 Elm",
|
|
|
});
|
|
|
expect(missing.some((m) => m.includes("提货") && m.includes("地址"))).toBe(
|
|
|
true,
|
|
|
);
|
|
|
expect(missing.some((m) => m.includes("送货") && m.includes("地址"))).toBe(
|
|
|
false,
|
|
|
);
|
|
|
});
|
|
|
|
|
|
it("提货与送货邮箱相同 → 拦截", () => {
|
|
|
const missing = collectMsDetailsRefineMissing(
|
|
|
fullDetails({
|
|
|
delivery: {
|
|
|
...fullDetails().delivery,
|
|
|
contactEmail: "pickup@acme.com",
|
|
|
},
|
|
|
}),
|
|
|
);
|
|
|
expect(missing.some((m) => m.includes("邮箱不能相同"))).toBe(true);
|
|
|
});
|
|
|
|
|
|
it("提货与送货邮箱大小写不同但相同 → 拦截", () => {
|
|
|
const missing = collectMsDetailsRefineMissing(
|
|
|
fullDetails({
|
|
|
pickup: {
|
|
|
...fullDetails().pickup,
|
|
|
contactEmail: "Same@Acme.com",
|
|
|
},
|
|
|
delivery: {
|
|
|
...fullDetails().delivery,
|
|
|
contactEmail: "same@acme.com",
|
|
|
},
|
|
|
}),
|
|
|
);
|
|
|
expect(missing.some((m) => m.includes("邮箱不能相同"))).toBe(true);
|
|
|
});
|
|
|
|
|
|
it("货运就绪时刻早于提货开门 → 拦截", () => {
|
|
|
const missing = collectMsDetailsRefineMissing(
|
|
|
fullDetails({
|
|
|
pickup: {
|
|
|
...fullDetails().pickup,
|
|
|
opensAt: "9:00 AM",
|
|
|
closesAt: "5:00 PM",
|
|
|
},
|
|
|
}),
|
|
|
{ readyTime: "7:00 AM" },
|
|
|
);
|
|
|
expect(missing.some((m) => m.includes("就绪时刻不能早于"))).toBe(true);
|
|
|
});
|
|
|
|
|
|
it("货运就绪时刻不早于开门 → 通过", () => {
|
|
|
expect(
|
|
|
validateMsDetailsForRefine(
|
|
|
fullDetails({
|
|
|
pickup: {
|
|
|
...fullDetails().pickup,
|
|
|
opensAt: "8:00 AM",
|
|
|
closesAt: "5:00 PM",
|
|
|
},
|
|
|
}),
|
|
|
{ readyTime: "11:00 AM" },
|
|
|
).ok,
|
|
|
).toBe(true);
|
|
|
});
|
|
|
|
|
|
it("开门不早于关门 → 拦截", () => {
|
|
|
const missing = collectMsDetailsRefineMissing(
|
|
|
fullDetails({
|
|
|
pickup: {
|
|
|
...fullDetails().pickup,
|
|
|
opensAt: "5:00 PM",
|
|
|
closesAt: "8:00 AM",
|
|
|
},
|
|
|
}),
|
|
|
);
|
|
|
expect(missing.some((m) => m.includes("开门时间须早于关门时间"))).toBe(
|
|
|
true,
|
|
|
);
|
|
|
});
|
|
|
|
|
|
it("snapMsReadyTimeToOpensAt 拨到开门整点", () => {
|
|
|
expect(snapMsReadyTimeToOpensAt("7:00 AM", "9:00 AM")).toBe("9:00 AM");
|
|
|
expect(snapMsReadyTimeToOpensAt("11:00 AM", "9:00 AM")).toBe("11:00 AM");
|
|
|
});
|
|
|
|
|
|
it("字段键覆盖空公司名与邮箱", () => {
|
|
|
const keys = collectMsDetailsRefineInvalidFields(
|
|
|
fullDetails({
|
|
|
pickup: {
|
|
|
...fullDetails().pickup,
|
|
|
companyName: "",
|
|
|
contactEmail: "bad",
|
|
|
},
|
|
|
}),
|
|
|
);
|
|
|
expect(keys.has("pickup.companyName")).toBe(true);
|
|
|
expect(keys.has("pickup.contactEmail")).toBe(true);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
vi.mock("@/lib/prisma", () => ({
|
|
|
prisma: {
|
|
|
quoteRecord: {
|
|
|
findUnique: vi.fn(),
|
|
|
update: vi.fn(),
|
|
|
},
|
|
|
},
|
|
|
}));
|
|
|
|
|
|
vi.mock("@/modules/address/parked-session-queue", () => ({
|
|
|
requestReleaseParkedSession: vi.fn().mockResolvedValue(undefined),
|
|
|
}));
|
|
|
|
|
|
vi.mock("@/modules/mothership/refine-queue", () => ({
|
|
|
enqueueMsRefineDetailsJob: vi.fn().mockResolvedValue(undefined),
|
|
|
}));
|
|
|
|
|
|
vi.mock("@/modules/quote/rpa-queue", () => ({
|
|
|
enqueueQuoteJob: vi.fn().mockResolvedValue(undefined),
|
|
|
}));
|
|
|
|
|
|
vi.mock("@/lib/mothership/dashboard-direct-quote", () => ({
|
|
|
fetchMothershipLoggedInDirectQuote: vi.fn(),
|
|
|
isMsDashboardDirectQuoteEnabled: vi.fn(() => true),
|
|
|
}));
|
|
|
|
|
|
vi.mock("@/lib/rpa/env", () => ({
|
|
|
isInlineDirectQuoteEnabled: vi.fn(() => true),
|
|
|
}));
|
|
|
|
|
|
vi.mock("@/lib/rpa/mothership-login-context", () => ({
|
|
|
runWithMothershipLoginContext: vi.fn((_login, _src, fn: () => unknown) => fn()),
|
|
|
}));
|
|
|
|
|
|
vi.mock("@/modules/customer/provider-credentials", () => ({
|
|
|
getCustomerProviderLogin: vi.fn().mockResolvedValue({
|
|
|
email: "ms@example.com",
|
|
|
password: "x",
|
|
|
}),
|
|
|
}));
|
|
|
|
|
|
vi.mock("@/modules/mothership/refine-apply-quotes", () => ({
|
|
|
applyMsRefineQuotes: vi.fn().mockResolvedValue(undefined),
|
|
|
}));
|
|
|
|
|
|
vi.mock("@/lib/mothership/refine-hold-store", () => ({
|
|
|
assertMsRefineHoldUsable: vi.fn(),
|
|
|
clearMsRefineHold: vi.fn().mockResolvedValue(undefined),
|
|
|
readMsRefineHold: vi.fn(),
|
|
|
updateMsRefineHoldStatus: vi.fn().mockResolvedValue(null),
|
|
|
}));
|
|
|
|
|
|
import {
|
|
|
continueMsRefineHold,
|
|
|
declineMsRefineHold,
|
|
|
submitMsRefineDetails,
|
|
|
} from "@/modules/mothership/refine-service";
|
|
|
import {
|
|
|
assertMsRefineHoldUsable,
|
|
|
clearMsRefineHold,
|
|
|
readMsRefineHold,
|
|
|
updateMsRefineHoldStatus,
|
|
|
} from "@/lib/mothership/refine-hold-store";
|
|
|
import { requestReleaseParkedSession } from "@/modules/address/parked-session-queue";
|
|
|
import { enqueueMsRefineDetailsJob } from "@/modules/mothership/refine-queue";
|
|
|
import { enqueueQuoteJob } from "@/modules/quote/rpa-queue";
|
|
|
import { prisma } from "@/lib/prisma";
|
|
|
import { fetchMothershipLoggedInDirectQuote } from "@/lib/mothership/dashboard-direct-quote";
|
|
|
import { applyMsRefineQuotes } from "@/modules/mothership/refine-apply-quotes";
|
|
|
import {
|
|
|
MOTHERSHIP_DELIVERY_CONFIRMED,
|
|
|
MOTHERSHIP_PICKUP_CONFIRMED,
|
|
|
} from "@/__tests__/fixtures/mothership-address";
|
|
|
|
|
|
describe("refine-service — 用户路径", () => {
|
|
|
const quoteId = "Q_TEST_1";
|
|
|
const customerId = "CUST_001";
|
|
|
const now = Date.now();
|
|
|
|
|
|
beforeEach(() => {
|
|
|
vi.clearAllMocks();
|
|
|
vi.mocked(fetchMothershipLoggedInDirectQuote).mockRejectedValue(
|
|
|
new Error("direct-disabled-in-test"),
|
|
|
);
|
|
|
});
|
|
|
|
|
|
it("decline:释放驻留并清 hold", async () => {
|
|
|
vi.mocked(readMsRefineHold).mockResolvedValue(
|
|
|
buildMsRefineHoldState({
|
|
|
quoteId,
|
|
|
sessionId: SESSION,
|
|
|
customerId,
|
|
|
nowMs: now,
|
|
|
}),
|
|
|
);
|
|
|
const r = await declineMsRefineHold({
|
|
|
customerId,
|
|
|
quoteId,
|
|
|
sessionId: SESSION,
|
|
|
});
|
|
|
expect(r.released).toBe(true);
|
|
|
expect(updateMsRefineHoldStatus).toHaveBeenCalledWith(SESSION, "declined");
|
|
|
expect(requestReleaseParkedSession).toHaveBeenCalledWith(SESSION);
|
|
|
expect(clearMsRefineHold).toHaveBeenCalledWith(SESSION);
|
|
|
});
|
|
|
|
|
|
it("decline:客户不匹配 → 拒绝", async () => {
|
|
|
vi.mocked(readMsRefineHold).mockResolvedValue(
|
|
|
buildMsRefineHoldState({
|
|
|
quoteId,
|
|
|
sessionId: SESSION,
|
|
|
customerId: "OTHER",
|
|
|
nowMs: now,
|
|
|
}),
|
|
|
);
|
|
|
await expect(
|
|
|
declineMsRefineHold({ customerId, quoteId, sessionId: SESSION }),
|
|
|
).rejects.toThrow(/无权/);
|
|
|
});
|
|
|
|
|
|
it("continue:awaiting → filling", async () => {
|
|
|
vi.mocked(readMsRefineHold).mockResolvedValue(
|
|
|
buildMsRefineHoldState({
|
|
|
quoteId,
|
|
|
sessionId: SESSION,
|
|
|
customerId,
|
|
|
nowMs: now,
|
|
|
}),
|
|
|
);
|
|
|
const r = await continueMsRefineHold({
|
|
|
customerId,
|
|
|
quoteId,
|
|
|
sessionId: SESSION,
|
|
|
});
|
|
|
expect(r.status).toBe("filling");
|
|
|
expect(updateMsRefineHoldStatus).toHaveBeenCalledWith(SESSION, "filling");
|
|
|
});
|
|
|
|
|
|
it("continue:决策窗已过仍可进入 filling(不再自动超时放弃)", async () => {
|
|
|
const s = buildMsRefineHoldState({
|
|
|
quoteId,
|
|
|
sessionId: SESSION,
|
|
|
customerId,
|
|
|
nowMs: now - MS_REFINE_DECISION_MS - 1,
|
|
|
});
|
|
|
vi.mocked(readMsRefineHold).mockResolvedValue(s);
|
|
|
const r = await continueMsRefineHold({
|
|
|
customerId,
|
|
|
quoteId,
|
|
|
sessionId: SESSION,
|
|
|
});
|
|
|
expect(r.status).toBe("filling");
|
|
|
expect(requestReleaseParkedSession).not.toHaveBeenCalled();
|
|
|
});
|
|
|
|
|
|
it("submit:会话窗已过 → 完整重查(不抛超时文案)", async () => {
|
|
|
const s = {
|
|
|
...buildMsRefineHoldState({
|
|
|
quoteId,
|
|
|
sessionId: SESSION,
|
|
|
customerId,
|
|
|
nowMs: now - MS_REFINE_TOTAL_MS - 1,
|
|
|
}),
|
|
|
status: "filling" as const,
|
|
|
};
|
|
|
vi.mocked(readMsRefineHold).mockResolvedValue(s);
|
|
|
vi.mocked(prisma.quoteRecord.findUnique).mockResolvedValue({
|
|
|
quoteId,
|
|
|
customerId,
|
|
|
requestId: "550e8400-e29b-41d4-a716-446655440000",
|
|
|
businessCustomerId: null,
|
|
|
pickupJson: {
|
|
|
street: "1234 Warehouse Blvd",
|
|
|
city: "Los Angeles",
|
|
|
state: "CA",
|
|
|
zip: "90001",
|
|
|
place_id: "ChIJ_pickup",
|
|
|
formatted_address: "1234 Warehouse Blvd, Los Angeles, CA 90001, USA",
|
|
|
selected_from_suggestions: true,
|
|
|
...MOTHERSHIP_PICKUP_CONFIRMED,
|
|
|
},
|
|
|
deliveryJson: {
|
|
|
street: "5678 Distribution Dr",
|
|
|
city: "Dallas",
|
|
|
state: "TX",
|
|
|
zip: "75201",
|
|
|
place_id: "ChIJ_delivery",
|
|
|
formatted_address: "5678 Distribution Dr, Dallas, TX 75201, USA",
|
|
|
selected_from_suggestions: true,
|
|
|
...MOTHERSHIP_DELIVERY_CONFIRMED,
|
|
|
},
|
|
|
weightLb: 500,
|
|
|
dimLIn: 48,
|
|
|
dimWIn: 40,
|
|
|
dimHIn: 48,
|
|
|
palletCount: 2,
|
|
|
cargoType: "general_freight",
|
|
|
} as never);
|
|
|
vi.mocked(prisma.quoteRecord.update).mockResolvedValue({} as never);
|
|
|
|
|
|
const r = await submitMsRefineDetails({
|
|
|
customerId,
|
|
|
quoteId,
|
|
|
sessionId: SESSION,
|
|
|
mothershipDetails: {
|
|
|
pickup: { company_name: "Acme", contact_email: "a@b.com" },
|
|
|
},
|
|
|
});
|
|
|
expect(r).toEqual({ quote_id: quoteId, status: "processing" });
|
|
|
expect(enqueueQuoteJob).toHaveBeenCalled();
|
|
|
expect(enqueueMsRefineDetailsJob).not.toHaveBeenCalled();
|
|
|
});
|
|
|
|
|
|
it("submit:filling 态入队刷价", async () => {
|
|
|
const s = {
|
|
|
...buildMsRefineHoldState({
|
|
|
quoteId,
|
|
|
sessionId: SESSION,
|
|
|
customerId,
|
|
|
nowMs: now,
|
|
|
}),
|
|
|
status: "filling" as const,
|
|
|
};
|
|
|
vi.mocked(assertMsRefineHoldUsable).mockResolvedValue(s);
|
|
|
vi.mocked(readMsRefineHold).mockResolvedValue(s);
|
|
|
vi.mocked(prisma.quoteRecord.findUnique).mockResolvedValue({
|
|
|
quoteId,
|
|
|
customerId,
|
|
|
requestId: "550e8400-e29b-41d4-a716-446655440000",
|
|
|
pickupJson: {
|
|
|
street: "1234 Warehouse Blvd",
|
|
|
city: "Los Angeles",
|
|
|
state: "CA",
|
|
|
zip: "90001",
|
|
|
place_id: "ChIJ_pickup",
|
|
|
formatted_address: "1234 Warehouse Blvd, Los Angeles, CA 90001, USA",
|
|
|
selected_from_suggestions: true,
|
|
|
...MOTHERSHIP_PICKUP_CONFIRMED,
|
|
|
},
|
|
|
deliveryJson: {
|
|
|
street: "5678 Distribution Dr",
|
|
|
city: "Dallas",
|
|
|
state: "TX",
|
|
|
zip: "75201",
|
|
|
place_id: "ChIJ_delivery",
|
|
|
formatted_address: "5678 Distribution Dr, Dallas, TX 75201, USA",
|
|
|
selected_from_suggestions: true,
|
|
|
...MOTHERSHIP_DELIVERY_CONFIRMED,
|
|
|
},
|
|
|
weightLb: 500,
|
|
|
dimLIn: 48,
|
|
|
dimWIn: 40,
|
|
|
dimHIn: 48,
|
|
|
palletCount: 2,
|
|
|
cargoType: "general_freight",
|
|
|
} as never);
|
|
|
vi.mocked(prisma.quoteRecord.update).mockResolvedValue({} as never);
|
|
|
|
|
|
const r = await submitMsRefineDetails({
|
|
|
customerId,
|
|
|
quoteId,
|
|
|
sessionId: SESSION,
|
|
|
mothershipDetails: {
|
|
|
pickup: { company_name: "Acme", contact_email: "a@b.com" },
|
|
|
},
|
|
|
});
|
|
|
expect(r).toEqual({ quote_id: quoteId, status: "processing" });
|
|
|
expect(updateMsRefineHoldStatus).toHaveBeenCalledWith(SESSION, "refining");
|
|
|
expect(enqueueMsRefineDetailsJob).toHaveBeenCalled();
|
|
|
});
|
|
|
|
|
|
it("submit:Direct 成功则跳过队列", async () => {
|
|
|
const s = {
|
|
|
...buildMsRefineHoldState({
|
|
|
quoteId,
|
|
|
sessionId: SESSION,
|
|
|
customerId,
|
|
|
nowMs: now,
|
|
|
}),
|
|
|
status: "filling" as const,
|
|
|
};
|
|
|
vi.mocked(assertMsRefineHoldUsable).mockResolvedValue(s);
|
|
|
vi.mocked(readMsRefineHold).mockResolvedValue(s);
|
|
|
vi.mocked(prisma.quoteRecord.findUnique).mockResolvedValue({
|
|
|
quoteId,
|
|
|
customerId,
|
|
|
requestId: "550e8400-e29b-41d4-a716-446655440000",
|
|
|
pickupJson: {
|
|
|
street: "1234 Warehouse Blvd",
|
|
|
city: "Los Angeles",
|
|
|
state: "CA",
|
|
|
zip: "90001",
|
|
|
place_id: "ChIJ_pickup",
|
|
|
formatted_address: "1234 Warehouse Blvd, Los Angeles, CA 90001, USA",
|
|
|
selected_from_suggestions: true,
|
|
|
...MOTHERSHIP_PICKUP_CONFIRMED,
|
|
|
},
|
|
|
deliveryJson: {
|
|
|
street: "5678 Distribution Dr",
|
|
|
city: "Dallas",
|
|
|
state: "TX",
|
|
|
zip: "75201",
|
|
|
place_id: "ChIJ_delivery",
|
|
|
formatted_address: "5678 Distribution Dr, Dallas, TX 75201, USA",
|
|
|
selected_from_suggestions: true,
|
|
|
...MOTHERSHIP_DELIVERY_CONFIRMED,
|
|
|
},
|
|
|
weightLb: 500,
|
|
|
dimLIn: 48,
|
|
|
dimWIn: 40,
|
|
|
dimHIn: 48,
|
|
|
palletCount: 2,
|
|
|
cargoType: "general_freight",
|
|
|
} as never);
|
|
|
vi.mocked(prisma.quoteRecord.update).mockResolvedValue({} as never);
|
|
|
vi.mocked(fetchMothershipLoggedInDirectQuote).mockResolvedValue([
|
|
|
{
|
|
|
serviceLevel: "standard",
|
|
|
rateOption: "bestValue",
|
|
|
carrier: "XPO Logistics Direct",
|
|
|
transitDays: "3",
|
|
|
transitDescription: "3 business days",
|
|
|
rawFreight: 400,
|
|
|
surcharges: 0,
|
|
|
rawTotal: 400,
|
|
|
},
|
|
|
]);
|
|
|
|
|
|
const r = await submitMsRefineDetails({
|
|
|
customerId,
|
|
|
quoteId,
|
|
|
sessionId: SESSION,
|
|
|
mothershipDetails: {
|
|
|
pickup: { company_name: "Acme", contact_email: "a@b.com" },
|
|
|
},
|
|
|
});
|
|
|
expect(r).toEqual({ quote_id: quoteId, status: "processing" });
|
|
|
expect(applyMsRefineQuotes).toHaveBeenCalled();
|
|
|
expect(enqueueMsRefineDetailsJob).not.toHaveBeenCalled();
|
|
|
expect(requestReleaseParkedSession).toHaveBeenCalled();
|
|
|
});
|
|
|
|
|
|
it("submit:报价单不属于客户 → 拒绝", async () => {
|
|
|
vi.mocked(readMsRefineHold).mockResolvedValue({
|
|
|
...buildMsRefineHoldState({
|
|
|
quoteId,
|
|
|
sessionId: SESSION,
|
|
|
customerId,
|
|
|
nowMs: now,
|
|
|
}),
|
|
|
status: "filling",
|
|
|
});
|
|
|
vi.mocked(prisma.quoteRecord.findUnique).mockResolvedValue({
|
|
|
quoteId,
|
|
|
customerId: "OTHER",
|
|
|
} as never);
|
|
|
await expect(
|
|
|
submitMsRefineDetails({
|
|
|
customerId,
|
|
|
quoteId,
|
|
|
sessionId: SESSION,
|
|
|
}),
|
|
|
).rejects.toThrow(/报价单不存在/);
|
|
|
});
|
|
|
});
|