/** * 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`; }