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.

290 lines
9.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 登录态刷价 hold:放弃 / 提交补充信息
*/
import { prisma } from "@/lib/prisma";
import { requestReleaseParkedSession } from "@/modules/address/parked-session-queue";
import {
assertMsRefineHoldUsable,
clearMsRefineHold,
readMsRefineHold,
updateMsRefineHoldStatus,
} from "@/lib/mothership/refine-hold-store";
import {
isMsRefineHoldExpired,
MS_REFINE_MSG,
} from "@/lib/constants/ms-refine-hold";
import { enqueueMsRefineDetailsJob } from "@/modules/mothership/refine-queue";
import { enqueueQuoteJob } from "@/modules/quote/rpa-queue";
import { validateQuoteInput } from "@/modules/quote/validation";
import type { QuoteRequestBody } from "@/lib/frontend/types";
import { applyMsRefineQuotes } from "@/modules/mothership/refine-apply-quotes";
import { cargoToQuoteRequest } from "@/modules/rpa/quote-mapper";
import { getCustomerProviderLogin } from "@/modules/customer/provider-credentials";
import { runWithMothershipLoginContext } from "@/lib/rpa/mothership-login-context";
import { isInlineDirectQuoteEnabled } from "@/lib/rpa/env";
import {
fetchMothershipLoggedInDirectQuote,
isMsDashboardDirectQuoteEnabled,
} from "@/lib/mothership/dashboard-direct-quote";
import {
MS_EMAILS_MUST_DIFFER_MESSAGE,
MS_READY_BEFORE_OPENS_MESSAGE,
parseMsClockToMinutes,
} from "@/lib/mothership/ms-details-refine-validation";
export async function declineMsRefineHold(input: {
customerId: string;
quoteId: string;
sessionId: string;
}): Promise<{ released: boolean }> {
const state = await readMsRefineHold(input.sessionId);
if (state && state.customer_id !== input.customerId) {
throw new Error("无权操作该报价会话");
}
if (state && state.quote_id !== input.quoteId) {
throw new Error("报价单与会话不匹配");
}
await updateMsRefineHoldStatus(input.sessionId, "declined");
await requestReleaseParkedSession(input.sessionId);
await clearMsRefineHold(input.sessionId);
return { released: true };
}
/** 用户确认继续填写:进入 filling,不再因决策窗自动放弃 */
export async function continueMsRefineHold(input: {
customerId: string;
quoteId: string;
sessionId: string;
}): Promise<{ status: "filling" }> {
const state = await readMsRefineHold(input.sessionId);
if (!state) {
throw new Error(MS_REFINE_MSG.sessionGone);
}
if (state.customer_id !== input.customerId) {
throw new Error("无权操作该报价会话");
}
if (state.quote_id !== input.quoteId) {
throw new Error("报价单与会话不匹配");
}
if (state.status === "declined" || state.status === "expired") {
throw new Error(MS_REFINE_MSG.sessionGone);
}
if (state.status === "refined") {
throw new Error("该报价已完成更新");
}
if (state.status === "filling" || state.status === "refining") {
return { status: "filling" };
}
await updateMsRefineHoldStatus(input.sessionId, "filling");
return { status: "filling" };
}
/**
* 提交补充信息刷价。
* 若驻留会话已过期:释放旧会话,用原地址 + 补充信息入队完整询价(对客户无超时提示)。
*/
export async function submitMsRefineDetails(input: {
customerId: string;
quoteId: string;
sessionId: string;
mothershipDetails?: QuoteRequestBody["mothership_details"];
pickupAccessorials?: string[];
deliveryAccessorials?: string[];
readyDate?: string;
readyTime?: string;
cargoLines?: QuoteRequestBody["cargo_lines"];
}): Promise<{ quote_id: string; status: "processing" }> {
const state = await readMsRefineHold(input.sessionId);
if (!state) {
throw new Error(MS_REFINE_MSG.sessionGone);
}
if (state.customer_id !== input.customerId) {
throw new Error("无权操作该报价会话");
}
if (state.quote_id !== input.quoteId) {
throw new Error("报价单与会话不匹配");
}
if (state.status === "declined" || state.status === "expired") {
throw new Error(MS_REFINE_MSG.sessionGone);
}
if (state.status === "refined") {
throw new Error("该报价已完成更新");
}
const record = await prisma.quoteRecord.findUnique({
where: { quoteId: input.quoteId },
});
if (!record || record.customerId !== input.customerId) {
throw new Error("报价单不存在");
}
const pickup = record.pickupJson as unknown as QuoteRequestBody["pickup_address"];
const delivery =
record.deliveryJson as unknown as QuoteRequestBody["delivery_address"];
const opensAt = input.mothershipDetails?.pickup?.opens_at?.trim() ?? "";
const readyTime = input.readyTime?.trim() || undefined;
if (readyTime && opensAt) {
const readyMin = parseMsClockToMinutes(readyTime);
const openMin = parseMsClockToMinutes(opensAt);
if (readyMin != null && openMin != null && readyMin < openMin) {
throw new Error(MS_READY_BEFORE_OPENS_MESSAGE);
}
}
const pickupEmail = String(
input.mothershipDetails?.pickup?.contact_email ?? "",
).trim();
const deliveryEmail = String(
input.mothershipDetails?.delivery?.contact_email ?? "",
).trim();
if (
pickupEmail &&
deliveryEmail &&
pickupEmail.toLowerCase() === deliveryEmail.toLowerCase()
) {
throw new Error(MS_EMAILS_MUST_DIFFER_MESSAGE);
}
const body = {
request_id: record.requestId,
customer_id: input.customerId,
pickup_address: pickup,
delivery_address: delivery,
weight: { value: Number(record.weightLb), unit: "lb" as const },
dimensions: {
length: Number(record.dimLIn),
width: Number(record.dimWIn),
height: Number(record.dimHIn),
unit: "in" as const,
},
pallet_count: record.palletCount,
cargo_type: record.cargoType as QuoteRequestBody["cargo_type"],
mothership_details: input.mothershipDetails,
pickup_accessorials: input.pickupAccessorials,
delivery_accessorials: input.deliveryAccessorials,
ready_date: input.readyDate,
ready_time: readyTime,
cargo_lines: input.cargoLines,
};
const expired = isMsRefineHoldExpired(state);
if (expired) {
// 会话过期:原地址 + 补充信息完整重查,不向客户暴露超时
await requestReleaseParkedSession(input.sessionId).catch(() => undefined);
await clearMsRefineHold(input.sessionId).catch(() => undefined);
const cargo = validateQuoteInput(body);
await prisma.quoteRecord.update({
where: { quoteId: input.quoteId },
data: { status: "processing", errorCode: null, errorMessage: null },
});
await enqueueQuoteJob({
quoteId: input.quoteId,
requestId: record.requestId,
customerId: input.customerId,
businessCustomerId: record.businessCustomerId ?? undefined,
cargoHash: cargo.cargoHash,
cargo,
});
return { quote_id: input.quoteId, status: "processing" };
}
// 仍可用驻留页:走刷价路径
try {
await assertMsRefineHoldUsable(input.sessionId);
} catch {
// 兜底:与过期相同,完整重查
await requestReleaseParkedSession(input.sessionId).catch(() => undefined);
await clearMsRefineHold(input.sessionId).catch(() => undefined);
const cargo = validateQuoteInput(body);
await prisma.quoteRecord.update({
where: { quoteId: input.quoteId },
data: { status: "processing", errorCode: null, errorMessage: null },
});
await enqueueQuoteJob({
quoteId: input.quoteId,
requestId: record.requestId,
customerId: input.customerId,
businessCustomerId: record.businessCustomerId ?? undefined,
cargoHash: cargo.cargoHash,
cargo,
});
return { quote_id: input.quoteId, status: "processing" };
}
const cargo = validateQuoteInput({
...body,
quote_session_id: input.sessionId,
});
cargo.quoteSessionId = input.sessionId;
await updateMsRefineHoldStatus(input.sessionId, "refining");
await prisma.quoteRecord.update({
where: { quoteId: input.quoteId },
data: { status: "processing", errorCode: null, errorMessage: null },
});
const inlined = await tryInlineRefineDirect(cargo, input.customerId);
if (inlined) {
await applyMsRefineQuotes({
quoteId: input.quoteId,
requestId: record.requestId,
customerId: input.customerId,
sessionId: input.sessionId,
cargo,
items: inlined,
});
await requestReleaseParkedSession(input.sessionId).catch(() => undefined);
return { quote_id: input.quoteId, status: "processing" };
}
await enqueueMsRefineDetailsJob({
quoteId: input.quoteId,
requestId: record.requestId,
customerId: input.customerId,
sessionId: input.sessionId,
cargo,
});
return { quote_id: input.quoteId, status: "processing" };
}
async function tryInlineRefineDirect(
cargo: ReturnType<typeof validateQuoteInput>,
customerId: string,
) {
if (!isMsDashboardDirectQuoteEnabled() || !isInlineDirectQuoteEnabled()) {
return null;
}
const customerLogin = await getCustomerProviderLogin(customerId, "mothership");
const email =
customerLogin?.email?.trim() || process.env.MOTHERSHIP_EMAIL?.trim() || "";
const password =
customerLogin?.password?.trim() ||
process.env.MOTHERSHIP_PASSWORD?.trim() ||
"";
if (!email) return null;
try {
const items = await runWithMothershipLoginContext(
{ email, password: password || "x" },
customerLogin ? "customer" : "env",
() =>
fetchMothershipLoggedInDirectQuote(cargoToQuoteRequest(cargo), {
allowBootstrap: false,
}),
);
if (items.length < 1) return null;
console.log(
`[ms-refine] inline-direct OK quote_id=${cargo.requestId} tiers=${items.length}`,
);
return items;
} catch (error) {
const brief = error instanceof Error ? error.message : String(error);
console.warn(
`[ms-refine] inline-direct 失败,回退队列 reason=${brief.slice(0, 180)}`,
);
return null;
}
}