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