import { randomUUID } from "node:crypto"; import { resolveMothershipCandidates } from "@/modules/address/mothership-candidates"; import type { MothershipAddressCandidate } from "@/modules/address/types"; import { applyHostCandidate } from "@/modules/host/address-mapper"; import { deleteHostPublicSession, loadHostPublicSession, saveHostPublicSession, } from "@/modules/host/public-session"; import type { HostPublicCandidatesResponse, HostPublicCargoInput, HostPublicAddressDraft, } from "@/modules/host/types"; import { HostQuoteWaitError, waitForQuoteDetail, } from "@/modules/host/wait-for-quote"; import { prisma } from "@/lib/prisma"; import { submitQuote } from "@/modules/quote/orchestrator"; import { serializeQuoteDetail, type QuoteDetailResponse, } from "@/modules/quote/quote-serializer"; export class HostPublicError extends Error { constructor( message: string, readonly code: string, ) { super(message); this.name = "HostPublicError"; } } async function loadQuoteDetailOnce( quoteId: string, customerId: string, ): Promise { const record = await prisma.quoteRecord.findFirst({ where: { quoteId, customerId, isDeleted: false }, }); if (!record) { throw new HostPublicError("报价不存在", "QUOTE_NOT_FOUND"); } return serializeQuoteDetail(record); } function findMatchingCandidate( candidates: MothershipAddressCandidate[], selected: MothershipAddressCandidate, label: string, ): MothershipAddressCandidate { if (selected.selectable === false) { throw new HostPublicError( selected.unavailable_reason ?? `${label}地址不可选`, "ADDRESS_NOT_SELECTABLE", ); } const matched = candidates.find((c) => c.option_id === selected.option_id); if (!matched) { throw new HostPublicError( `${label}地址不在本次联想结果中,请重新发起联想`, "ADDRESS_SESSION_MISMATCH", ); } if (matched.selectable === false) { throw new HostPublicError( matched.unavailable_reason ?? `${label}地址不可选`, "ADDRESS_NOT_SELECTABLE", ); } return matched; } /** 第一步:地址 + 货物 → 联想候选 + host_session_id */ export async function hostPublicResolveCandidates( customerId: string, input: { pickup_address: HostPublicAddressDraft; delivery_address: HostPublicAddressDraft; cargo: HostPublicCargoInput; }, ): Promise { const pickup = { ...input.pickup_address, zip: input.pickup_address.zip ?? "", }; const delivery = { ...input.delivery_address, zip: input.delivery_address.zip ?? "", }; const resolved = await resolveMothershipCandidates({ pickup, delivery }); const host_session_id = await saveHostPublicSession({ customer_id: customerId, cargo: input.cargo, pickup_draft: pickup, delivery_draft: delivery, quote_session_id: resolved.quote_session_id, pickup_candidates: resolved.pickup_candidates, delivery_candidates: resolved.delivery_candidates, }); return { host_session_id, pickup_candidates: resolved.pickup_candidates, delivery_candidates: resolved.delivery_candidates, requires_selection: resolved.requires_selection, }; } /** 第二步:确认联想地址 → 同步等待 → 返回最终报价 */ export async function hostPublicSubmitAndWait(input: { host_session_id: string; pickup_candidate: MothershipAddressCandidate; delivery_candidate: MothershipAddressCandidate; }): Promise { const session = await loadHostPublicSession(input.host_session_id); if (!session) { throw new HostPublicError("会话已过期,请重新发起联想", "SESSION_EXPIRED"); } const pickup = findMatchingCandidate( session.pickup_candidates, input.pickup_candidate, "提货", ); const delivery = findMatchingCandidate( session.delivery_candidates, input.delivery_candidate, "派送", ); const body = { request_id: randomUUID(), customer_id: session.customer_id, quote_session_id: session.quote_session_id, pickup_address: applyHostCandidate(session.pickup_draft, pickup), delivery_address: applyHostCandidate(session.delivery_draft, delivery), weight: session.cargo.weight, dimensions: session.cargo.dimensions, pallet_count: session.cargo.pallet_count, cargo_type: session.cargo.cargo_type, }; const created = await submitQuote(body); if (!created.quote_id) { throw new HostPublicError("创建询价失败", "QUOTE_CREATE_FAILED"); } try { if (created.status === "done") { return await loadQuoteDetailOnce(created.quote_id, session.customer_id); } return await waitForQuoteDetail(created.quote_id, session.customer_id); } finally { await deleteHostPublicSession(input.host_session_id); } } export { HostQuoteWaitError };