import { Prisma } from "@prisma/client"; import { getL2 } from "@/modules/cache/redis-cache"; import { QUOTE_VALIDITY_MS } from "@/lib/constants/quote"; import { prisma } from "@/lib/prisma"; import { getConfidenceScore } from "@/modules/quote/confidence"; import { applyMarkupToQuotes, getMarkupRule, type RawQuoteTier, } from "@/modules/pricing/engine"; import { checkIdempotency, saveIdempotency, } from "@/modules/quote/idempotency"; import { assertFourTiers } from "@/modules/quote/quote-completeness"; import { generateQuoteId, handleQuoteIdConflict, isQuoteIdUniqueConflict, } from "@/modules/quote/quote-id"; import { enqueueQuoteJob } from "@/modules/quote/rpa-queue"; import type { L2CachePayload, NormalizedCargo, QuoteSubmitResult, } from "@/modules/quote/types"; import { markQuoteStarted, recordCacheHit, recordPostDone, recordPostTotal, recordRpaTriggered, safeRecord, } from "@/modules/metrics/collector"; 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; } 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)); assertFourTiers(l2.quotes); const markupRule = await getMarkupRule(cargo.customerId); const markedQuotes = applyMarkupToQuotes( l2.quotes as RawQuoteTier[], 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, ); return response; } 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, }); return response; } export type { NormalizedCargo, QuoteSubmitResult };