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.

165 lines
4.8 KiB

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<QuoteDetailResponse> {
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<HostPublicCandidatesResponse> {
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<QuoteDetailResponse> {
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 };