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
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" };
|
|
}
|