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.

261 lines
7.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* Priority1 表单填写规则与站点硬限制RPA / 系统联调 / canonical 用例校验)
* 来源form-logic-map 探测、批量验收、人工录制与页面校验文案
*/
import type { Priority1DemoInput } from "@/workers/rpa/priority1/demo-input";
import type {
ExpeditedTrailerType,
FtlTrailerType,
Priority1ShipmentType,
} from "@/workers/rpa/priority1/shipment-types";
import { expeditedLiftgateAvailable } from "@/workers/rpa/priority1/shipment-types";
export type Priority1ConstraintSeverity = "error" | "warning";
export type Priority1ConstraintViolation = {
severity: Priority1ConstraintSeverity;
code: string;
message: string;
field?: string;
};
/** 互斥与结构规则(选 A 则不能/不必选 B */
export const PRIORITY1_MUTEX_RULES: readonly {
id: string;
rule: string;
}[] = [
{
id: "shipment_type_one",
rule: "Step1 运输类型三选一LTL / FTL / Expedited不可多选",
},
{
id: "ftl_trailer_one",
rule: "FTL Step2 主拖车四选一Dry Van / Open Deck / Temperature Controlled / Expedited内嵌",
},
{
id: "expedited_trailer_one",
rule: "Expedited 主类型 Step2 拖车三选一Sprinter / Small Straight / Large Straight",
},
{
id: "ftl_expedited_sub_one",
rule: "FTL 内嵌 Expedited 须再选子车型三选一(同 Expedited 三卡)",
},
{
id: "open_deck_requires_subtype",
rule: "选 Open Deck 后必须选 Open Deck Type#PickupLocation3否则无法继续",
},
{
id: "temp_requires_range",
rule: "选 Temperature Controlled 后必须填最低/最高温度°F",
},
{
id: "liftgate_large_only",
rule: "Liftgate 仅在选择 Large Straight Box Truck 时出现且有意义Expedited 主类型或 FTL 内嵌 Expedited",
},
{
id: "no_pallets_expedited",
rule: "Expedited 主类型与 FTL 内嵌 Expedited 无 Number of Pallets 字段",
},
{
id: "ftl_non_expedited_has_pallets",
rule: "FTL Dry Van / Open Deck / Temperature 须填托盘数",
},
{
id: "zip_distinct",
rule: "起运 ZIP 与目的 ZIP 不能相同",
},
];
/** 重量/托盘硬上限(超限时站点拦截提交或无法即时报价) */
export const PRIORITY1_WEIGHT_LIMITS: Record<
string,
{ maxLb: number; minLb?: number; maxPallets?: number; siteMessage?: string }
> = {
"expedited:sprinter_van": {
minLb: 1,
maxLb: 3500,
siteMessage: "Sprinter 卡片标注约 3500 lb 上限",
},
"expedited:small_straight": {
minLb: 1,
maxLb: 8000,
},
"expedited:large_straight": {
minLb: 1,
maxLb: 10000,
maxPallets: 14,
siteMessage:
"Expedited shipments must be 10,000 lbs or less; for overweight choose Full Truckload Dry Van with Team Drivers",
},
"ftl:dry_van": { minLb: 1, maxLb: 45000, maxPallets: 26 },
"ftl:open_deck": { minLb: 1, maxLb: 45000, maxPallets: 22 },
"ftl:temperature_controlled": { minLb: 1, maxLb: 40000, maxPallets: 24 },
/** FTL 内嵌 Expedited 子车型沿用 Expedited 拖车重量规则 */
"ftl_expedited:sprinter_van": { minLb: 1, maxLb: 3500 },
"ftl_expedited:small_straight": { minLb: 1, maxLb: 8000 },
"ftl_expedited:large_straight": { minLb: 1, maxLb: 10000, maxPallets: 14 },
};
function weightKey(
shipmentType: Priority1ShipmentType,
ftlTrailer?: FtlTrailerType,
expeditedTrailer?: ExpeditedTrailerType,
): string | null {
if (shipmentType === "expedited" && expeditedTrailer) {
return `expedited:${expeditedTrailer}`;
}
if (shipmentType === "ftl" && ftlTrailer === "ftl_expedited" && expeditedTrailer) {
return `ftl_expedited:${expeditedTrailer}`;
}
if (shipmentType === "ftl" && ftlTrailer && ftlTrailer !== "ftl_expedited") {
return `ftl:${ftlTrailer}`;
}
return null;
}
/** 校验 canonical / API 入参是否违反已知站点规则 */
export function validatePriority1FormConstraints(
demo: Priority1DemoInput,
): Priority1ConstraintViolation[] {
const out: Priority1ConstraintViolation[] = [];
const zipOk = /^\d{5}$/;
if (!zipOk.test(demo.originZip) || !zipOk.test(demo.destinationZip)) {
out.push({
severity: "error",
code: "ZIP_FORMAT",
message: "ZIP 须为 5 位数字",
field: "originZip",
});
}
if (demo.originZip === demo.destinationZip) {
out.push({
severity: "error",
code: "ZIP_SAME",
message: "起运与目的 ZIP 不能相同",
field: "destinationZip",
});
}
if (demo.shipmentType === "ftl" && demo.ftlTrailer === "open_deck" && !demo.openDeckSubtype?.trim()) {
out.push({
severity: "error",
code: "OPEN_DECK_SUBTYPE",
message: "Open Deck 必须选择子类型",
field: "openDeckSubtype",
});
}
if (demo.shipmentType === "ftl" && demo.ftlTrailer === "temperature_controlled") {
if (!demo.tempMinF?.trim() || !demo.tempMaxF?.trim()) {
out.push({
severity: "error",
code: "TEMP_RANGE",
message: "温控须填写最低与最高温度",
field: "tempMinF",
});
} else if (Number(demo.tempMinF) > Number(demo.tempMaxF)) {
out.push({
severity: "error",
code: "TEMP_ORDER",
message: "最低温度不能高于最高温度",
field: "tempMinF",
});
}
}
if (
demo.expeditedLiftgate &&
!expeditedLiftgateAvailable(
demo.shipmentType === "ftl"
? demo.ftlExpeditedTrailer
: demo.expeditedTrailer,
)
) {
out.push({
severity: "error",
code: "LIFTGATE_TRAILER",
message: "Liftgate 仅适用于 Large Straight Box Truck",
field: "expeditedLiftgate",
});
}
const wKey = weightKey(
demo.shipmentType,
demo.ftlTrailer,
demo.shipmentType === "ftl" ? demo.ftlExpeditedTrailer : demo.expeditedTrailer,
);
const weight = Number(demo.weightLb);
if (wKey && Number.isFinite(weight)) {
const lim = PRIORITY1_WEIGHT_LIMITS[wKey];
if (lim) {
if (lim.minLb != null && weight < lim.minLb) {
out.push({
severity: "warning",
code: "WEIGHT_LOW",
message: `${wKey} 建议重量 ≥ ${lim.minLb} lb`,
field: "weightLb",
});
}
if (weight > lim.maxLb) {
out.push({
severity: "error",
code: "WEIGHT_MAX",
message: `重量 ${weight} lb 超过 ${wKey} 上限 ${lim.maxLb} lb${lim.siteMessage ? `${lim.siteMessage}` : ""}`,
field: "weightLb",
});
}
}
}
if (
demo.shipmentType === "ftl" &&
demo.ftlTrailer !== "ftl_expedited" &&
demo.palletCount
) {
const pallets = Number(demo.palletCount);
const lim = PRIORITY1_WEIGHT_LIMITS[`ftl:${demo.ftlTrailer}`];
if (lim?.maxPallets != null && pallets > lim.maxPallets) {
out.push({
severity: "error",
code: "PALLETS_MAX",
message: `托盘数 ${pallets} 超过 ${demo.ftlTrailer} 建议上限 ${lim.maxPallets}`,
field: "palletCount",
});
}
}
return out;
}
export function formatPriority1ConstraintsMarkdown(): string {
const lines = [
"# Priority1 填写规则与限制",
"",
"## 互斥与结构",
"",
...PRIORITY1_MUTEX_RULES.map((r) => `- **${r.id}**${r.rule}`),
"",
"## 重量 / 托盘上限lb",
"",
"| 场景 | 最小 | 最大重量 | 最大托盘 | 备注 |",
"|------|------|----------|----------|------|",
];
for (const [key, lim] of Object.entries(PRIORITY1_WEIGHT_LIMITS)) {
lines.push(
`| ${key} | ${lim.minLb ?? "—"} | ${lim.maxLb} | ${lim.maxPallets ?? "—"} | ${lim.siteMessage ?? ""} |`,
);
}
lines.push(
"",
"## 报价终态(非表单错误)",
"",
"- **即时报价**LTL 卡片 / FTL Dry Van·Open Deck 部分子类型 日历报价",
"- **人工跟进**:温控、部分 Open Deck、FTL/主类型 Expedited 等 → 「MORE INFORMATION NEEDED」",
"- 人工跟进 **不算 RPA 失败**,返回:表单已提交,站点判定需人工确认细节后报价",
"",
);
return `${lines.join("\n")}\n`;
}