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.

145 lines
4.2 KiB

import { Prisma } from "@prisma/client";
import { prisma } from "@/lib/prisma";
import { markQuoteStarted, safeRecord } from "@/modules/metrics/collector";
import { generateQuoteId, handleQuoteIdConflict, isQuoteIdUniqueConflict } from "@/modules/quote/quote-id";
import type { Priority1DemoInput } from "@/workers/rpa/priority1/demo-input";
import { hashPriority1Input } from "@/modules/priority1/hash";
import { enqueuePriority1QuoteJob } from "@/modules/priority1/rpa-queue";
import { validatePriority1QuoteInput } from "@/modules/priority1/validation";
export class Priority1EnqueueError extends Error {
readonly code = "QUOTE_UNAVAILABLE" as const;
constructor(message: string) {
super(message);
this.name = "Priority1EnqueueError";
}
}
export type Priority1SubmitResult = {
quote_id: string;
status: "processing";
};
function zipAddressJson(
zip: string,
input: Priority1DemoInput,
side: "origin" | "destination",
): Prisma.InputJsonValue {
return {
street: "",
city: "",
state: "",
zip,
place_id: "",
formatted_address: zip,
selected_from_suggestions: false,
...(side === "origin"
? {
priority1_shipment_type: input.shipmentType,
priority1_pickup_date: input.pickupDate,
priority1_commodity: input.commodity,
}
: {}),
};
}
function recordFields(input: Priority1DemoInput) {
const weight = Number(input.weightLb) || 0;
const length = Number(input.lengthIn) || 48;
const width = Number(input.widthIn) || 40;
const height = Number(input.heightIn) || 48;
const pallets = Number(input.palletCount) || 1;
const cargoType =
input.shipmentType === "ltl"
? input.packaging.toLowerCase()
: input.commodity.toLowerCase().replace(/\s+/g, "_").slice(0, 32);
return {
weightLb: weight,
dimLIn: length,
dimWIn: width,
dimHIn: height,
palletCount: pallets,
cargoType: cargoType || "general_freight",
};
}
/** Priority1 询价入队 */
export async function submitPriority1Quote(body: unknown): Promise<Priority1SubmitResult> {
const parsed = validatePriority1QuoteInput(body);
const { request_id, customer_id, ...input } = parsed;
const cargoHash = hashPriority1Input(input);
let quoteId = await generateQuoteId();
const dims = recordFields(input);
const baseRecord = {
requestId: request_id,
customerId: customer_id,
cargoHash,
status: "processing" as const,
pickupJson: zipAddressJson(input.originZip, input, "origin"),
deliveryJson: zipAddressJson(input.destinationZip, input, "destination"),
weightLb: dims.weightLb,
dimLIn: dims.dimLIn,
dimWIn: dims.dimWIn,
dimHIn: dims.dimHIn,
palletCount: dims.palletCount,
cargoType: dims.cargoType,
quotesJson: Prisma.JsonNull,
};
try {
await prisma.quoteRecord.create({
data: { quoteId, ...baseRecord },
});
} catch (error) {
if (!isQuoteIdUniqueConflict(error)) {
throw error;
}
quoteId = await generateQuoteId();
try {
await prisma.quoteRecord.create({
data: { quoteId, ...baseRecord },
});
} catch (retryError) {
if (isQuoteIdUniqueConflict(retryError)) {
await handleQuoteIdConflict(quoteId, customer_id);
}
throw retryError;
}
}
try {
await enqueuePriority1QuoteJob({
quoteId,
requestId: request_id,
customerId: customer_id,
cargoHash,
input,
});
} catch (error) {
const brief = error instanceof Error ? error.message : String(error);
console.error(`[priority1] 入队失败 quote_id=${quoteId}:`, error);
const redisLike = /redis|REDIS_URL/i.test(brief);
const userMessage = redisLike
? "Redis 不可用,无法提交 Priority1 询价(请确认 REDIS_URL 与 Worker 已启动)"
: "Priority1 任务入队失败,请稍后重试或联系管理员";
await prisma.quoteRecord
.update({
where: { quoteId },
data: {
status: "failed",
errorCode: "QUOTE_UNAVAILABLE",
errorMessage: userMessage,
},
})
.catch(() => undefined);
throw new Priority1EnqueueError(userMessage);
}
safeRecord(() => markQuoteStarted(quoteId));
return { quote_id: quoteId, status: "processing" };
}