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
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 };
|