|
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
|
|
|
vi.mock("@/modules/address/mothership-candidates", () => ({
|
|
|
resolveMothershipCandidates: vi.fn(),
|
|
|
}));
|
|
|
|
|
|
vi.mock("@/modules/host/public-session", () => ({
|
|
|
saveHostPublicSession: vi.fn(),
|
|
|
loadHostPublicSession: vi.fn(),
|
|
|
deleteHostPublicSession: vi.fn(),
|
|
|
}));
|
|
|
|
|
|
vi.mock("@/modules/quote/orchestrator", () => ({
|
|
|
submitQuote: vi.fn(),
|
|
|
}));
|
|
|
|
|
|
vi.mock("@/modules/host/wait-for-quote", () => ({
|
|
|
waitForQuoteDetail: vi.fn(),
|
|
|
HostQuoteWaitError: class HostQuoteWaitError extends Error {
|
|
|
code = "QUOTE_FAILED";
|
|
|
detail = undefined;
|
|
|
},
|
|
|
}));
|
|
|
|
|
|
vi.mock("@/lib/prisma", () => ({
|
|
|
prisma: {
|
|
|
quoteRecord: {
|
|
|
findFirst: vi.fn(),
|
|
|
},
|
|
|
},
|
|
|
}));
|
|
|
|
|
|
import { resolveMothershipCandidates } from "@/modules/address/mothership-candidates";
|
|
|
import {
|
|
|
deleteHostPublicSession,
|
|
|
loadHostPublicSession,
|
|
|
saveHostPublicSession,
|
|
|
} from "@/modules/host/public-session";
|
|
|
import {
|
|
|
hostPublicResolveCandidates,
|
|
|
hostPublicSubmitAndWait,
|
|
|
} from "@/modules/host/public-orchestrator";
|
|
|
import { waitForQuoteDetail } from "@/modules/host/wait-for-quote";
|
|
|
import { submitQuote } from "@/modules/quote/orchestrator";
|
|
|
import { prisma } from "@/lib/prisma";
|
|
|
|
|
|
const pickupCandidate = {
|
|
|
option_id: "pickup-opt-1",
|
|
|
display_label: "1234 Warehouse St, Los Angeles, CA, USA",
|
|
|
formatted_address: "1234 Warehouse St, Los Angeles, CA, USA",
|
|
|
street: "1234 Warehouse St",
|
|
|
city: "Los Angeles",
|
|
|
state: "CA",
|
|
|
zip: "90001",
|
|
|
selectable: true,
|
|
|
};
|
|
|
|
|
|
const deliveryCandidate = {
|
|
|
option_id: "delivery-opt-1",
|
|
|
display_label: "5678 Distribution Dr, Dallas, TX, USA",
|
|
|
formatted_address: "5678 Distribution Dr, Dallas, TX, USA",
|
|
|
street: "5678 Distribution Dr",
|
|
|
city: "Dallas",
|
|
|
state: "TX",
|
|
|
zip: "75201",
|
|
|
selectable: true,
|
|
|
};
|
|
|
|
|
|
describe("hostPublicResolveCandidates", () => {
|
|
|
beforeEach(() => {
|
|
|
vi.clearAllMocks();
|
|
|
vi.mocked(saveHostPublicSession).mockResolvedValue("session-uuid");
|
|
|
});
|
|
|
|
|
|
it("returns host_session_id and candidates", async () => {
|
|
|
vi.mocked(resolveMothershipCandidates).mockResolvedValue({
|
|
|
pickup_candidates: [pickupCandidate],
|
|
|
delivery_candidates: [deliveryCandidate],
|
|
|
requires_selection: false,
|
|
|
quote_session_id: "quote-session-1",
|
|
|
});
|
|
|
|
|
|
const result = await hostPublicResolveCandidates("CUST_002", {
|
|
|
pickup_address: {
|
|
|
street: "1234 Warehouse Blvd",
|
|
|
city: "Los Angeles",
|
|
|
state: "CA",
|
|
|
zip: "90001",
|
|
|
},
|
|
|
delivery_address: {
|
|
|
street: "5678 Distribution Dr",
|
|
|
city: "Dallas",
|
|
|
state: "TX",
|
|
|
zip: "75201",
|
|
|
},
|
|
|
cargo: {
|
|
|
weight: { value: 500, unit: "kg" },
|
|
|
dimensions: { length: 120, width: 100, height: 150, unit: "cm" },
|
|
|
pallet_count: 2,
|
|
|
cargo_type: "general_freight",
|
|
|
},
|
|
|
});
|
|
|
|
|
|
expect(result.host_session_id).toBe("session-uuid");
|
|
|
expect(result.pickup_candidates).toHaveLength(1);
|
|
|
expect(saveHostPublicSession).toHaveBeenCalledWith(
|
|
|
expect.objectContaining({ customer_id: "CUST_002" }),
|
|
|
);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("hostPublicSubmitAndWait", () => {
|
|
|
beforeEach(() => {
|
|
|
vi.clearAllMocks();
|
|
|
vi.mocked(loadHostPublicSession).mockResolvedValue({
|
|
|
customer_id: "CUST_001",
|
|
|
cargo: {
|
|
|
weight: { value: 500, unit: "kg" },
|
|
|
dimensions: { length: 120, width: 100, height: 150, unit: "cm" },
|
|
|
pallet_count: 2,
|
|
|
cargo_type: "general_freight",
|
|
|
},
|
|
|
pickup_draft: {
|
|
|
street: "1234 Warehouse Blvd",
|
|
|
city: "Los Angeles",
|
|
|
state: "CA",
|
|
|
zip: "90001",
|
|
|
},
|
|
|
delivery_draft: {
|
|
|
street: "5678 Distribution Dr",
|
|
|
city: "Dallas",
|
|
|
state: "TX",
|
|
|
zip: "75201",
|
|
|
},
|
|
|
quote_session_id: "quote-session-1",
|
|
|
pickup_candidates: [pickupCandidate],
|
|
|
delivery_candidates: [deliveryCandidate],
|
|
|
created_at: new Date().toISOString(),
|
|
|
});
|
|
|
vi.mocked(submitQuote).mockResolvedValue({
|
|
|
quote_id: "QTE_test_001",
|
|
|
status: "processing",
|
|
|
});
|
|
|
vi.mocked(waitForQuoteDetail).mockResolvedValue({
|
|
|
quote_id: "QTE_test_001",
|
|
|
request_id: "req-1",
|
|
|
status: "done",
|
|
|
currency: "USD",
|
|
|
quotes: [{ final_total: 337.06 }],
|
|
|
});
|
|
|
vi.mocked(prisma.quoteRecord.findFirst).mockResolvedValue({
|
|
|
quoteId: "QTE_test_001",
|
|
|
requestId: "req-1",
|
|
|
status: "done",
|
|
|
sourceType: "rpa",
|
|
|
isRealtime: true,
|
|
|
currency: "USD",
|
|
|
confidenceScore: 0.95,
|
|
|
quotesJson: [{ final_total: 337.06 }],
|
|
|
validUntil: new Date("2026-06-30T10:00:00.000Z"),
|
|
|
createdAt: new Date("2026-06-30T09:55:00.000Z"),
|
|
|
palletCount: 2,
|
|
|
cargoType: "general_freight",
|
|
|
errorCode: null,
|
|
|
errorMessage: null,
|
|
|
isDeleted: false,
|
|
|
} as never);
|
|
|
});
|
|
|
|
|
|
it("submits quote and waits for final detail", async () => {
|
|
|
const detail = await hostPublicSubmitAndWait({
|
|
|
host_session_id: "session-uuid",
|
|
|
pickup_candidate: pickupCandidate,
|
|
|
delivery_candidate: deliveryCandidate,
|
|
|
});
|
|
|
|
|
|
expect(submitQuote).toHaveBeenCalledOnce();
|
|
|
expect(waitForQuoteDetail).toHaveBeenCalledWith(
|
|
|
"QTE_test_001",
|
|
|
"CUST_001",
|
|
|
);
|
|
|
expect(deleteHostPublicSession).toHaveBeenCalledWith("session-uuid");
|
|
|
expect(detail.status).toBe("done");
|
|
|
});
|
|
|
|
|
|
it("rejects candidate not in session", async () => {
|
|
|
await expect(
|
|
|
hostPublicSubmitAndWait({
|
|
|
host_session_id: "session-uuid",
|
|
|
pickup_candidate: { ...pickupCandidate, option_id: "unknown" },
|
|
|
delivery_candidate: deliveryCandidate,
|
|
|
}),
|
|
|
).rejects.toMatchObject({ code: "ADDRESS_SESSION_MISMATCH" });
|
|
|
});
|
|
|
|
|
|
it("submitQuote 已 done 时直接查一次 DB,不走 waitForQuoteDetail", async () => {
|
|
|
vi.mocked(submitQuote).mockResolvedValue({
|
|
|
quote_id: "QTE_test_001",
|
|
|
status: "done",
|
|
|
source_type: "rpa",
|
|
|
is_realtime: true,
|
|
|
});
|
|
|
|
|
|
const detail = await hostPublicSubmitAndWait({
|
|
|
host_session_id: "session-uuid",
|
|
|
pickup_candidate: pickupCandidate,
|
|
|
delivery_candidate: deliveryCandidate,
|
|
|
});
|
|
|
|
|
|
expect(prisma.quoteRecord.findFirst).toHaveBeenCalledWith({
|
|
|
where: {
|
|
|
quoteId: "QTE_test_001",
|
|
|
customerId: "CUST_001",
|
|
|
isDeleted: false,
|
|
|
},
|
|
|
});
|
|
|
expect(waitForQuoteDetail).not.toHaveBeenCalled();
|
|
|
expect(detail.status).toBe("done");
|
|
|
});
|
|
|
});
|