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.

180 lines
5.1 KiB

import type { MarkupRule } from "@/modules/pricing/engine";
import { applyMarkup } from "@/modules/pricing/engine";
import type { Priority1QuoteLine } from "@/workers/rpa/priority1/quote-extract";
export type Priority1DisplayMode = "ltl_cards" | "ftl_calendar";
export type Priority1DisplayLine = Priority1QuoteLine & {
markup_amount: number;
final_total_usd: number;
};
export type Priority1StoredQuotes = {
provider: "priority1";
display_mode: Priority1DisplayMode;
lines: Priority1DisplayLine[];
};
export function inferPriority1DisplayMode(
lines: Priority1QuoteLine[],
shipmentType?: string,
): Priority1DisplayMode {
if (shipmentType === "ftl") {
return "ftl_calendar";
}
if (
lines.some(
(l) =>
l.source === "ftl-calendar" ||
/ftl\s*calendar/i.test(l.serviceLevel) ||
l.carrier.includes("FTL 日历"),
)
) {
return "ftl_calendar";
}
return "ltl_cards";
}
export function applyMarkupToPriority1Lines(
lines: Priority1QuoteLine[],
rule: MarkupRule,
): Priority1DisplayLine[] {
return lines.map((line) => {
const { markupAmount, finalTotal } = applyMarkup(
line.totalUsd,
line.totalUsd,
rule,
);
return {
...line,
markup_amount: markupAmount,
final_total_usd: finalTotal,
};
});
}
export function buildPriority1StoredQuotes(
lines: Priority1QuoteLine[],
rule: MarkupRule,
shipmentType?: string,
): Priority1StoredQuotes {
const marked = applyMarkupToPriority1Lines(lines, rule);
return {
provider: "priority1",
display_mode: inferPriority1DisplayMode(lines, shipmentType),
lines: marked,
};
}
export function parsePriority1StoredQuotes(
json: unknown,
): Priority1StoredQuotes | null {
if (!json || typeof json !== "object" || Array.isArray(json)) {
return null;
}
const o = json as Record<string, unknown>;
if (o.provider !== "priority1" || !Array.isArray(o.lines)) {
return null;
}
return o as unknown as Priority1StoredQuotes;
}
/** 从 pickupJson 读取 Priority1 填单上下文 */
export function readPriority1ShipmentMeta(
pickupJson: unknown,
): { shipmentType?: string; pickupDate?: string; commodity?: string } {
if (!pickupJson || typeof pickupJson !== "object") {
return {};
}
const o = pickupJson as Record<string, unknown>;
return {
shipmentType:
typeof o.priority1_shipment_type === "string"
? o.priority1_shipment_type
: undefined,
pickupDate:
typeof o.priority1_pickup_date === "string"
? o.priority1_pickup_date
: undefined,
commodity:
typeof o.priority1_commodity === "string"
? o.priority1_commodity
: undefined,
};
}
type LegacyQuoteTierRow = {
service_level?: string;
rate_option?: string;
carrier?: string;
transit_days?: string;
raw_total?: number;
final_total?: number;
markup_amount?: number;
};
/** 旧版 MotherShip tier 数组 → Priority1 展示行(兼容未升级的 worker / stale 降级) */
export function legacyTiersToPriority1StoredQuotes(
tiers: LegacyQuoteTierRow[],
shipmentType?: string,
): Priority1StoredQuotes | null {
const lines: Priority1DisplayLine[] = [];
for (const tier of tiers) {
const raw = Number(tier.raw_total ?? tier.final_total ?? 0);
const final = Number(tier.final_total ?? tier.raw_total ?? 0);
if (!Number.isFinite(raw) || raw <= 0) continue;
lines.push({
rank: lines.length + 1,
carrier: String(tier.carrier ?? "Unknown").trim() || "Unknown",
carrierCode: "",
totalUsd: raw,
markup_amount: Number(tier.markup_amount ?? Math.max(0, final - raw)) || 0,
final_total_usd: final > 0 ? final : raw,
transitDays: Number.parseInt(String(tier.transit_days ?? ""), 10) || 0,
deliveryDate: null,
expirationDate: null,
serviceLevel: "STANDARD LTL",
quoteId: null,
caboUrl: null,
source: "legacy-tier",
});
}
if (lines.length === 0) return null;
return {
provider: "priority1",
display_mode: inferPriority1DisplayMode(lines, shipmentType),
lines,
};
}
function isLegacyQuoteTierArray(json: unknown): json is LegacyQuoteTierRow[] {
if (!Array.isArray(json) || json.length === 0) return false;
const row = json[0];
if (!row || typeof row !== "object") return false;
const o = row as Record<string, unknown>;
return (
typeof o.service_level === "string" &&
(typeof o.raw_total === "number" || typeof o.final_total === "number")
);
}
/** 从 DB quotesJson 解析 Priority1 展示数据(含旧 tier 兼容) */
export function resolvePriority1StoredQuotes(
quotesJson: unknown,
pickupJson?: unknown,
): Priority1StoredQuotes | null {
const stored = parsePriority1StoredQuotes(quotesJson);
if (stored && stored.lines.length > 0) {
return stored;
}
const meta = readPriority1ShipmentMeta(pickupJson);
// MotherShip tier 数组与 Priority1 旧 tier 结构相似;仅 Priority1 询价上下文才做 legacy 转换
if (!meta.shipmentType) {
return null;
}
if (isLegacyQuoteTierArray(quotesJson)) {
return legacyTiersToPriority1StoredQuotes(quotesJson, meta.shipmentType);
}
return null;
}