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.

207 lines
5.8 KiB

import type { QuoteRecord } from "@prisma/client";
import { filterQuotesForMotherShipUiDisplay } from "@/lib/constants/mothership-ui-tiers";
import { PRIORITY1_MANUAL_FOLLOWUP_ERROR_CODE } from "@/modules/priority1/constants";
import {
readPriority1ShipmentMeta,
resolvePriority1StoredQuotes,
type Priority1DisplayLine,
} from "@/modules/priority1/quote-storage";
import { getConfidenceScore } from "@/modules/quote/confidence";
import { formatQuoteErrorMessage } from "@/modules/quote/quote-error-messages";
type QuoteTierKey = { service_level: string; rate_option: string };
export type Priority1QuoteDetailPayload = {
display_mode: "ltl_cards" | "ftl_calendar";
lines: Priority1DisplayLine[];
shipment_meta?: {
shipment_type?: string;
pickup_date?: string;
commodity?: string;
origin_zip?: string;
destination_zip?: string;
weight_lb?: number;
};
};
export type QuoteDetailResponse = {
quote_id: string;
request_id: string;
status: "processing" | "done" | "failed" | "expired";
valid_until?: string;
source_type?: "rpa" | "cache" | "stale";
is_realtime?: boolean;
confidence_score?: number;
currency: string;
error_code?: string;
error_message?: string;
quotes?: unknown[];
priority1?: Priority1QuoteDetailPayload;
};
export type QuoteSummaryItem = {
quote_id: string;
request_id: string;
status: string;
source_type: string | null;
is_realtime: boolean;
created_at: string;
valid_until: string | null;
pallet_count: number;
cargo_type: string;
error_code: string | null;
};
function resolveStatus(record: QuoteRecord): QuoteDetailResponse["status"] {
if (
record.status === "done" &&
record.validUntil &&
record.validUntil.getTime() < Date.now()
) {
return "expired";
}
return record.status as QuoteDetailResponse["status"];
}
/** 将 QuoteRecord 格式化为 GET /quotes/{id} 响应 */
export function serializeQuoteDetail(record: QuoteRecord): QuoteDetailResponse {
const status = resolveStatus(record);
const sourceType = record.sourceType as
| "rpa"
| "cache"
| "stale"
| null
| undefined;
const base: QuoteDetailResponse = {
quote_id: record.quoteId,
request_id: record.requestId,
status,
currency: record.currency,
};
if (status === "processing") {
return base;
}
if (status === "failed") {
const errorCode = record.errorCode ?? "QUOTE_UNAVAILABLE";
return {
...base,
error_code: errorCode,
error_message: formatQuoteErrorMessage(errorCode, record.errorMessage),
is_realtime: record.isRealtime,
};
}
if (status === "expired") {
return {
...base,
valid_until: record.validUntil?.toISOString(),
source_type: sourceType ?? undefined,
is_realtime: record.isRealtime,
};
}
if (
record.errorCode === PRIORITY1_MANUAL_FOLLOWUP_ERROR_CODE
) {
const confidence =
record.confidenceScore != null
? Number(record.confidenceScore)
: getConfidenceScore("rpa");
return {
...base,
source_type: sourceType ?? "rpa",
is_realtime: record.isRealtime,
confidence_score: confidence,
error_code: PRIORITY1_MANUAL_FOLLOWUP_ERROR_CODE,
error_message: formatQuoteErrorMessage(
record.errorCode,
record.errorMessage,
),
quotes: [],
};
}
const confidence =
record.confidenceScore != null
? Number(record.confidenceScore)
: sourceType
? getConfidenceScore(sourceType)
: 0.95;
const priority1Stored = resolvePriority1StoredQuotes(
record.quotesJson,
record.pickupJson,
);
const priority1Meta = readPriority1ShipmentMeta(record.pickupJson);
if (priority1Stored) {
const pickup = record.pickupJson as Record<string, unknown> | null;
const delivery = record.deliveryJson as Record<string, unknown> | null;
const meta = readPriority1ShipmentMeta(record.pickupJson);
return {
...base,
valid_until: record.validUntil?.toISOString(),
source_type: sourceType ?? undefined,
is_realtime: record.isRealtime,
confidence_score: confidence,
priority1: {
display_mode: priority1Stored.display_mode,
lines: priority1Stored.lines,
shipment_meta: {
shipment_type: meta.shipmentType,
pickup_date: meta.pickupDate,
commodity: meta.commodity,
origin_zip:
typeof pickup?.zip === "string" ? pickup.zip : undefined,
destination_zip:
typeof delivery?.zip === "string" ? delivery.zip : undefined,
weight_lb:
record.weightLb != null ? Number(record.weightLb) : undefined,
},
},
};
}
if (priority1Meta.shipmentType) {
const errorCode = record.errorCode ?? "QUOTE_UNAVAILABLE";
return {
...base,
status: "failed",
error_code: errorCode,
error_message: formatQuoteErrorMessage(errorCode, record.errorMessage),
is_realtime: record.isRealtime,
confidence_score: confidence,
};
}
return {
...base,
valid_until: record.validUntil?.toISOString(),
source_type: sourceType ?? undefined,
is_realtime: record.isRealtime,
confidence_score: confidence,
quotes: filterQuotesForMotherShipUiDisplay(
((record.quotesJson as QuoteTierKey[]) ?? []) as QuoteTierKey[],
),
};
}
/** 历史列表摘要 */
export function serializeQuoteSummary(record: QuoteRecord): QuoteSummaryItem {
const status = resolveStatus(record);
return {
quote_id: record.quoteId,
request_id: record.requestId,
status,
source_type: record.sourceType,
is_realtime: record.isRealtime,
created_at: record.createdAt.toISOString(),
valid_until: record.validUntil?.toISOString() ?? null,
pallet_count: record.palletCount,
cargo_type: record.cargoType,
error_code: record.errorCode,
};
}