import { Prisma } from "@prisma/client"; import { getL2, setL2, setL3 } from "@/modules/cache/redis-cache"; import { QUOTE_VALIDITY_MS } from "@/lib/constants/quote"; import { prisma } from "@/lib/prisma"; import { fetchAxelQuoteItems } from "@/lib/axel/quote-from-request"; import { isAxelDirectQuoteMode, isInlineDirectQuoteEnabled } from "@/lib/rpa/env"; import { getConfidenceScore } from "@/modules/quote/confidence"; import { applyMarkupToQuotes, getMarkupRule, type RawQuoteTier, } from "@/modules/pricing/engine"; import { checkIdempotency, saveIdempotency, } from "@/modules/quote/idempotency"; import { prepareMotherShipStorageQuotes } from "@/modules/quote/quote-completeness"; import { generateQuoteId, handleQuoteIdConflict, isQuoteIdUniqueConflict, } from "@/modules/quote/quote-id"; import { enqueueQuoteJob } from "@/modules/quote/rpa-queue"; import { recordQuoteQueryOutcome, recordQuoteQueryStart, } from "@/modules/quote/query-log"; import type { L2CachePayload, NormalizedCargo, QuoteSubmitResult, } from "@/modules/quote/types"; import { markQuoteStarted, recordCacheHit, recordPostDone, recordPostTotal, recordRpaTriggered, safeRecord, } from "@/modules/metrics/collector"; import { cargoToQuoteRequest, quoteItemsToRawTiers } from "@/modules/rpa/quote-mapper"; import { validateQuoteInput } from "@/modules/quote/validation"; export type SubmitQuoteResponse = { quote_id: string; status: "processing" | "done"; source_type?: "cache" | "rpa" | "stale"; is_realtime?: boolean; }; /** 询价主编排:校验 → L1 → L2 → 入队(task-014) */ export async function submitQuote( body: unknown, ): Promise { const cargo = validateQuoteInput(body); safeRecord(() => recordPostTotal()); const idem = await checkIdempotency(cargo.requestId); if (idem.hit) { safeRecord(() => recordCacheHit("l1")); const cached = idem.response as SubmitQuoteResponse; if (cached?.status === "done") { safeRecord(() => recordPostDone({ quoteId: idem.quoteId, sourceType: cached.source_type ?? "cache", isRealtime: cached.is_realtime ?? true, }), ); } return { quote_id: idem.quoteId, status: (cached?.status as "processing" | "done") ?? "done", source_type: cached?.source_type, is_realtime: cached?.is_realtime, }; } const l2 = await getL2(cargo.cargoHash); if (l2?.quotes?.length) { const result = await completeFromCache(cargo, l2); safeRecord(() => recordCacheHit("l2")); safeRecord(() => recordPostDone({ quoteId: result.quote_id, sourceType: "cache", isRealtime: true, }), ); return result; } if (isAxelDirectQuoteMode() && isInlineDirectQuoteEnabled()) { const inline = await tryInlineDirectQuote(cargo); if (inline) { safeRecord(() => recordPostDone({ quoteId: inline.quote_id, sourceType: "rpa", isRealtime: true, }), ); return inline; } } const result = await enqueueForRpa(cargo); safeRecord(() => recordRpaTriggered(result.quote_id)); return result; } async function completeFromCache( cargo: NormalizedCargo, l2: L2CachePayload, ): Promise { const quoteId = await generateQuoteId(); safeRecord(() => markQuoteStarted(quoteId)); const uiQuotes = prepareMotherShipStorageQuotes(l2.quotes as RawQuoteTier[]); const markupRule = await getMarkupRule(cargo.customerId); const markedQuotes = applyMarkupToQuotes(uiQuotes, markupRule); const validUntil = new Date(Date.now() + QUOTE_VALIDITY_MS); const response: SubmitQuoteResponse = { quote_id: quoteId, status: "done", source_type: "cache", is_realtime: true, }; try { await prisma.quoteRecord.create({ data: { quoteId, requestId: cargo.requestId, customerId: cargo.customerId, cargoHash: cargo.cargoHash, status: "done", sourceType: "cache", isRealtime: true, confidenceScore: getConfidenceScore("cache"), pickupJson: cargo.pickupAddress, deliveryJson: cargo.deliveryAddress, weightLb: cargo.weightLb, dimLIn: cargo.dimLIn, dimWIn: cargo.dimWIn, dimHIn: cargo.dimHIn, palletCount: cargo.palletCount, cargoType: cargo.cargoType, quotesJson: markedQuotes as unknown as Prisma.InputJsonValue, markupPercent: markupRule.type === "percent" ? markupRule.percent : 0, validUntil, }, }); } catch (error) { if (isQuoteIdUniqueConflict(error)) { await handleQuoteIdConflict(quoteId, cargo.customerId); } throw error; } await saveIdempotency( cargo.requestId, cargo.customerId, quoteId, response, ); await recordQuoteQueryStart(cargo, quoteId); await recordQuoteQueryOutcome(quoteId, "success", { sourceType: "cache", tierCount: markedQuotes.length, }); return response; } async function tryInlineDirectQuote( cargo: NormalizedCargo, ): Promise { const quoteId = await generateQuoteId(); safeRecord(() => markQuoteStarted(quoteId)); try { const items = await fetchAxelQuoteItems(cargoToQuoteRequest(cargo)); const rawQuotes = quoteItemsToRawTiers(items); const uiQuotes = prepareMotherShipStorageQuotes(rawQuotes); const markupRule = await getMarkupRule(cargo.customerId); const markedQuotes = applyMarkupToQuotes(uiQuotes, markupRule); const validUntil = new Date(Date.now() + QUOTE_VALIDITY_MS); const cachePayload = { quotes: uiQuotes }; await setL2(cargo.cargoHash, cachePayload); await setL3(cargo.cargoHash, cachePayload); const response: SubmitQuoteResponse = { quote_id: quoteId, status: "done", source_type: "rpa", is_realtime: true, }; try { await prisma.quoteRecord.create({ data: { quoteId, requestId: cargo.requestId, customerId: cargo.customerId, cargoHash: cargo.cargoHash, status: "done", sourceType: "rpa", isRealtime: true, confidenceScore: getConfidenceScore("rpa"), pickupJson: cargo.pickupAddress, deliveryJson: cargo.deliveryAddress, weightLb: cargo.weightLb, dimLIn: cargo.dimLIn, dimWIn: cargo.dimWIn, dimHIn: cargo.dimHIn, palletCount: cargo.palletCount, cargoType: cargo.cargoType, quotesJson: markedQuotes as unknown as Prisma.InputJsonValue, markupPercent: markupRule.type === "percent" ? markupRule.percent : 0, validUntil, }, }); } catch (error) { if (isQuoteIdUniqueConflict(error)) { await handleQuoteIdConflict(quoteId, cargo.customerId); } throw error; } await saveIdempotency( cargo.requestId, cargo.customerId, quoteId, response, ); await recordQuoteQueryStart(cargo, quoteId); await recordQuoteQueryOutcome(quoteId, "success", { sourceType: "rpa", tierCount: markedQuotes.length, }); return response; } catch (error) { const brief = error instanceof Error ? error.message : String(error); console.warn( `[quote] inline-direct 失败,回退队列 cargo_hash=${cargo.cargoHash} reason=${brief.slice(0, 200)}`, ); return null; } } async function enqueueForRpa( cargo: NormalizedCargo, ): Promise { const quoteId = await generateQuoteId(); const markupRule = await getMarkupRule(cargo.customerId); const response: SubmitQuoteResponse = { quote_id: quoteId, status: "processing", }; try { await prisma.quoteRecord.create({ data: { quoteId, requestId: cargo.requestId, customerId: cargo.customerId, cargoHash: cargo.cargoHash, status: "processing", isRealtime: true, pickupJson: cargo.pickupAddress, deliveryJson: cargo.deliveryAddress, weightLb: cargo.weightLb, dimLIn: cargo.dimLIn, dimWIn: cargo.dimWIn, dimHIn: cargo.dimHIn, palletCount: cargo.palletCount, cargoType: cargo.cargoType, markupPercent: markupRule.type === "percent" ? markupRule.percent : 0, }, }); } catch (error) { if (isQuoteIdUniqueConflict(error)) { await handleQuoteIdConflict(quoteId, cargo.customerId); } throw error; } await saveIdempotency( cargo.requestId, cargo.customerId, quoteId, response, ); await enqueueQuoteJob({ quoteId, requestId: cargo.requestId, customerId: cargo.customerId, cargoHash: cargo.cargoHash, cargo, }); await recordQuoteQueryStart(cargo, quoteId); return response; } export type { NormalizedCargo, QuoteSubmitResult };