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