|
|
/**
|
|
|
* 登录态刷价 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;
|
|
|
}
|
|
|
}
|