|
|
/** Priority1 公开询价页演示/批量测试输入 */
|
|
|
|
|
|
import type {
|
|
|
ExpeditedTrailerType,
|
|
|
FtlAdditionalService,
|
|
|
FtlTrailerType,
|
|
|
Priority1ShipmentType,
|
|
|
} from "@/workers/rpa/priority1/shipment-types";
|
|
|
import {
|
|
|
EXPEDITED_TRAILER,
|
|
|
FTL_ADDITIONAL,
|
|
|
FTL_TRAILER,
|
|
|
getShipmentConfig,
|
|
|
} from "@/workers/rpa/priority1/shipment-types";
|
|
|
|
|
|
export type Priority1DemoInput = {
|
|
|
shipmentType: Priority1ShipmentType;
|
|
|
originZip: string;
|
|
|
destinationZip: string;
|
|
|
shipmentFrequency: string;
|
|
|
pickupDate: string;
|
|
|
email: string;
|
|
|
phone: string;
|
|
|
phoneCountry: string;
|
|
|
/** LTL */
|
|
|
lengthIn: string;
|
|
|
widthIn: string;
|
|
|
heightIn: string;
|
|
|
weightLb: string;
|
|
|
palletCount: string;
|
|
|
freightClass: string;
|
|
|
packaging: string;
|
|
|
pickupLocation: string;
|
|
|
deliveryLocation: string;
|
|
|
pickupServices: string[];
|
|
|
deliveryServices: string[];
|
|
|
/** FTL / Expedited 共用 */
|
|
|
commodity: string;
|
|
|
/** FTL */
|
|
|
ftlTrailer: FtlTrailerType;
|
|
|
ftlAdditionalServices: FtlAdditionalService[];
|
|
|
/** Expedited 主类型 Step 2 拖车 */
|
|
|
expeditedTrailer: ExpeditedTrailerType;
|
|
|
/** FTL Open Deck 子类型 */
|
|
|
openDeckSubtype: string;
|
|
|
/** FTL 温控 */
|
|
|
tempMinF: string;
|
|
|
tempMaxF: string;
|
|
|
/** FTL 选 Expedited 拖车后的子车型 */
|
|
|
ftlExpeditedTrailer: ExpeditedTrailerType;
|
|
|
/** 仅 Large Straight 可选 Liftgate */
|
|
|
expeditedLiftgate: boolean;
|
|
|
/** FTL 内嵌 Expedited — 特殊处理要求(可选) */
|
|
|
specialHandlingServices: string[];
|
|
|
};
|
|
|
|
|
|
const COMMON_STEP1 = {
|
|
|
shipmentFrequency: "One Time Shipment",
|
|
|
email: "niqijiti542@gmail.com",
|
|
|
};
|
|
|
|
|
|
/** P08 canonical — LTL 全附加服务(11 项) */
|
|
|
export const P08_LTL_ALL_ACCESSORIALS = {
|
|
|
pickup: [
|
|
|
"Liftgate at Pickup",
|
|
|
"Inside Pickup",
|
|
|
"Limited Access Pickup",
|
|
|
"Residential Pickup",
|
|
|
],
|
|
|
delivery: [
|
|
|
"Liftgate at Delivery",
|
|
|
"Inside Delivery",
|
|
|
"Delivery Appointment",
|
|
|
"Call Before Delivery",
|
|
|
"Limited Access Delivery",
|
|
|
"Hazardous Material",
|
|
|
"Residential Delivery",
|
|
|
],
|
|
|
} as const;
|
|
|
|
|
|
export const P08_LTL_ACCESSORIAL_LABELS: readonly string[] = [
|
|
|
...P08_LTL_ALL_ACCESSORIALS.pickup,
|
|
|
...P08_LTL_ALL_ACCESSORIALS.delivery,
|
|
|
];
|
|
|
|
|
|
export const DEFAULT_PRIORITY1_DEMO: Priority1DemoInput = {
|
|
|
shipmentType: "ltl",
|
|
|
originZip: "10118",
|
|
|
destinationZip: "90017",
|
|
|
pickupDate: "07/15/2026",
|
|
|
phone: "2125550198",
|
|
|
phoneCountry: "US",
|
|
|
...COMMON_STEP1,
|
|
|
lengthIn: "48",
|
|
|
widthIn: "40",
|
|
|
heightIn: "48",
|
|
|
weightLb: "30",
|
|
|
palletCount: "2",
|
|
|
freightClass: "70",
|
|
|
packaging: "Pallet",
|
|
|
pickupLocation: "Residence / Home Business",
|
|
|
deliveryLocation: "Residence / Home Business",
|
|
|
pickupServices: ["Liftgate at Pickup", "Residential Pickup"],
|
|
|
deliveryServices: ["Liftgate at Delivery", "Residential Delivery"],
|
|
|
commodity: "General Freight",
|
|
|
ftlTrailer: "dry_van",
|
|
|
ftlAdditionalServices: [],
|
|
|
expeditedTrailer: "sprinter_van",
|
|
|
openDeckSubtype: "48 Foot Flatbed",
|
|
|
tempMinF: "32",
|
|
|
tempMaxF: "40",
|
|
|
ftlExpeditedTrailer: "sprinter_van",
|
|
|
expeditedLiftgate: false,
|
|
|
specialHandlingServices: [],
|
|
|
};
|
|
|
|
|
|
export const DEFAULT_FTL_DEMO: Priority1DemoInput = {
|
|
|
...DEFAULT_PRIORITY1_DEMO,
|
|
|
shipmentType: "ftl",
|
|
|
originZip: "32801",
|
|
|
destinationZip: "28202",
|
|
|
pickupDate: "07/14/2026",
|
|
|
phone: "7536407420",
|
|
|
commodity: "Electronics",
|
|
|
weightLb: "15000",
|
|
|
palletCount: "18",
|
|
|
ftlTrailer: "dry_van",
|
|
|
ftlAdditionalServices: ["full_ftl"],
|
|
|
};
|
|
|
|
|
|
/** FTL Open Deck — 见 form-logic-map FTL_CONDITIONAL_BRANCHES.ftl_open_deck */
|
|
|
export const DEFAULT_FTL_OPEN_DECK_DEMO: Priority1DemoInput = {
|
|
|
...DEFAULT_FTL_DEMO,
|
|
|
ftlTrailer: "open_deck",
|
|
|
openDeckSubtype: "48 Foot Flatbed",
|
|
|
ftlAdditionalServices: ["full_ftl"],
|
|
|
commodity: "Electronics",
|
|
|
weightLb: "12000",
|
|
|
palletCount: "14",
|
|
|
};
|
|
|
|
|
|
/** FTL Temperature Controlled — 见 form-logic-map FTL_CONDITIONAL_BRANCHES.ftl_temp */
|
|
|
export const DEFAULT_FTL_TEMP_DEMO: Priority1DemoInput = {
|
|
|
...DEFAULT_FTL_DEMO,
|
|
|
ftlTrailer: "temperature_controlled",
|
|
|
ftlAdditionalServices: ["full_ftl"],
|
|
|
commodity: "Electronics",
|
|
|
tempMinF: "32",
|
|
|
tempMaxF: "40",
|
|
|
weightLb: "10000",
|
|
|
palletCount: "16",
|
|
|
};
|
|
|
|
|
|
export const DEFAULT_EXPEDITED_DEMO: Priority1DemoInput = {
|
|
|
...DEFAULT_PRIORITY1_DEMO,
|
|
|
shipmentType: "expedited",
|
|
|
originZip: "32801",
|
|
|
destinationZip: "28202",
|
|
|
pickupDate: "07/14/2026",
|
|
|
phone: "7536407420",
|
|
|
commodity: "Electronics",
|
|
|
weightLb: "800",
|
|
|
expeditedTrailer: "sprinter_van",
|
|
|
};
|
|
|
|
|
|
/** FTL 内嵌 Expedited — 见 form-logic-map FTL_CONDITIONAL_BRANCHES.ftl_expedited_sub */
|
|
|
export const DEFAULT_FTL_FTL_EXPEDITED_DEMO: Priority1DemoInput = {
|
|
|
...DEFAULT_FTL_DEMO,
|
|
|
ftlTrailer: "ftl_expedited",
|
|
|
ftlAdditionalServices: [],
|
|
|
ftlExpeditedTrailer: "sprinter_van",
|
|
|
commodity: "Electronics",
|
|
|
weightLb: "2500",
|
|
|
expeditedLiftgate: false,
|
|
|
};
|
|
|
|
|
|
export type Priority1CanonicalPath = {
|
|
|
id: string;
|
|
|
title: string;
|
|
|
/** 用户画像 / 需求简述 */
|
|
|
persona: string;
|
|
|
input: Priority1DemoInput;
|
|
|
/** recorded=已录制或 visual chain 跑通;logic-only=仅逻辑映射待录制 */
|
|
|
verified: "recorded" | "logic-only";
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 全部 canonical 路径 — 覆盖 Priority1 表单各决策分支与代表性选项组合。
|
|
|
* 录制:npm run record:priority1 -- 后按各路径 id 手动走一遍。
|
|
|
*/
|
|
|
export const PRIORITY1_CANONICAL_PATHS: Priority1CanonicalPath[] = [
|
|
|
{
|
|
|
id: "P01-ltl-residential",
|
|
|
title: "LTL 住宅 + 常用附加",
|
|
|
persona: "电商卖家,住宅提货派送,需尾板",
|
|
|
input: DEFAULT_PRIORITY1_DEMO,
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "P02-ltl-business-dock",
|
|
|
title: "LTL 商业有装卸台",
|
|
|
persona: "仓库出货,双方均有 dock/forklift",
|
|
|
input: {
|
|
|
...DEFAULT_PRIORITY1_DEMO,
|
|
|
pickupLocation: "Business (with loading docks or forklift)",
|
|
|
deliveryLocation: "Business (with loading docks or forklift)",
|
|
|
pickupServices: [],
|
|
|
deliveryServices: [],
|
|
|
},
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "P03-ltl-business-no-dock",
|
|
|
title: "LTL 商业无 dock",
|
|
|
persona: "小型门店,需尾板提货/派送",
|
|
|
input: {
|
|
|
...DEFAULT_PRIORITY1_DEMO,
|
|
|
pickupLocation: "Business (no loading dock, no forklift)",
|
|
|
deliveryLocation: "Business (no loading dock, no forklift)",
|
|
|
pickupServices: ["Liftgate at Pickup"],
|
|
|
deliveryServices: ["Liftgate at Delivery"],
|
|
|
},
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "P04-ltl-crate",
|
|
|
title: "LTL 包装 Crate",
|
|
|
persona: "机械零件,木箱包装非托盘",
|
|
|
input: {
|
|
|
...DEFAULT_PRIORITY1_DEMO,
|
|
|
packaging: "Crate",
|
|
|
freightClass: "85",
|
|
|
},
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "P05-ltl-box",
|
|
|
title: "LTL 包装 Box",
|
|
|
persona: "小件快递式纸箱散货",
|
|
|
input: { ...DEFAULT_PRIORITY1_DEMO, packaging: "Box", palletCount: "1" },
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "P06-ltl-bundle",
|
|
|
title: "LTL 包装 Bundle",
|
|
|
persona: "管材/长条物料捆扎",
|
|
|
input: { ...DEFAULT_PRIORITY1_DEMO, packaging: "Bundle", freightClass: "60" },
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "P07-ltl-not-sure-class",
|
|
|
title: "LTL Freight Class 不确定",
|
|
|
persona: "首次发货,不清楚 NMFC 等级",
|
|
|
input: { ...DEFAULT_PRIORITY1_DEMO, freightClass: "Not Sure" },
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "P08-ltl-all-accessorials",
|
|
|
title: "LTL 全附加服务",
|
|
|
persona: "限制进入区 + 室内 + 预约通知",
|
|
|
input: {
|
|
|
...DEFAULT_PRIORITY1_DEMO,
|
|
|
pickupServices: [...P08_LTL_ALL_ACCESSORIALS.pickup],
|
|
|
deliveryServices: [...P08_LTL_ALL_ACCESSORIALS.delivery],
|
|
|
},
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "P09-ltl-frequent-shipper",
|
|
|
title: "LTL 高频发货",
|
|
|
persona: "月发 11+ 票的大客户",
|
|
|
input: { ...DEFAULT_PRIORITY1_DEMO, shipmentFrequency: "100+" },
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "F01-ftl-dry-van-full",
|
|
|
title: "FTL Dry Van + Full FTL",
|
|
|
persona: "整车普货,53ft 干柜",
|
|
|
input: DEFAULT_FTL_DEMO,
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "F02-ftl-dry-van-partial",
|
|
|
title: "FTL Dry Van + Partial PTL",
|
|
|
persona: "拼车/部分整车",
|
|
|
input: {
|
|
|
...DEFAULT_FTL_DEMO,
|
|
|
ftlAdditionalServices: ["partial_ptl"],
|
|
|
weightLb: "8000",
|
|
|
palletCount: "10",
|
|
|
},
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "F03-ftl-dry-van-team",
|
|
|
title: "FTL Dry Van + Team Drivers",
|
|
|
persona: "跨州加急,双驾不换班",
|
|
|
input: {
|
|
|
...DEFAULT_FTL_DEMO,
|
|
|
ftlAdditionalServices: ["team_drivers"],
|
|
|
weightLb: "38000",
|
|
|
palletCount: "22",
|
|
|
},
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "F04-ftl-open-deck-flatbed",
|
|
|
title: "FTL Open Deck Flatbed",
|
|
|
persona: "超宽设备,平板车",
|
|
|
input: DEFAULT_FTL_OPEN_DECK_DEMO,
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "F05-ftl-open-deck-step",
|
|
|
title: "FTL Open Deck Step Deck",
|
|
|
persona: "较高货物,阶梯平板",
|
|
|
input: {
|
|
|
...DEFAULT_FTL_OPEN_DECK_DEMO,
|
|
|
openDeckSubtype: "48 Foot Step Deck",
|
|
|
},
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "F06-ftl-open-deck-conestoga",
|
|
|
title: "FTL Open Deck Conestoga",
|
|
|
persona: "防雨布侧帘平板",
|
|
|
input: {
|
|
|
...DEFAULT_FTL_OPEN_DECK_DEMO,
|
|
|
openDeckSubtype: "Conestoga",
|
|
|
},
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "F07-ftl-open-deck-rgn",
|
|
|
title: "FTL Open Deck Goose Neck RGN",
|
|
|
persona: "重型机械低板",
|
|
|
input: {
|
|
|
...DEFAULT_FTL_OPEN_DECK_DEMO,
|
|
|
openDeckSubtype: "Removable Gooseneck",
|
|
|
weightLb: "35000",
|
|
|
},
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "F08-ftl-temperature",
|
|
|
title: "FTL Temperature Controlled",
|
|
|
persona: "冷链食品 32–40°F",
|
|
|
input: DEFAULT_FTL_TEMP_DEMO,
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "F09-ftl-expedited-sprinter",
|
|
|
title: "FTL 内嵌 Expedited Sprinter",
|
|
|
persona: "FTL 页选第四卡 Expedited + Sprinter",
|
|
|
input: DEFAULT_FTL_FTL_EXPEDITED_DEMO,
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "F10-ftl-expedited-small",
|
|
|
title: "FTL 内嵌 Expedited Small Straight",
|
|
|
persona: "FTL Expedited 小直卡",
|
|
|
input: {
|
|
|
...DEFAULT_FTL_FTL_EXPEDITED_DEMO,
|
|
|
ftlExpeditedTrailer: "small_straight",
|
|
|
weightLb: "5000",
|
|
|
},
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "F11-ftl-expedited-large-liftgate",
|
|
|
title: "FTL 内嵌 Expedited Large + Liftgate",
|
|
|
persona: "大直卡 + 尾板卸货",
|
|
|
input: {
|
|
|
...DEFAULT_FTL_FTL_EXPEDITED_DEMO,
|
|
|
ftlExpeditedTrailer: "large_straight",
|
|
|
weightLb: "10000",
|
|
|
expeditedLiftgate: true,
|
|
|
},
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "E01-expedited-sprinter",
|
|
|
title: "Expedited 主类型 Sprinter",
|
|
|
persona: "加急小件 ≤3500 lb",
|
|
|
input: DEFAULT_EXPEDITED_DEMO,
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "E02-expedited-small",
|
|
|
title: "Expedited Small Straight",
|
|
|
persona: "加急中型直卡",
|
|
|
input: {
|
|
|
...DEFAULT_EXPEDITED_DEMO,
|
|
|
expeditedTrailer: "small_straight",
|
|
|
weightLb: "4500",
|
|
|
},
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "E03-expedited-large",
|
|
|
title: "Expedited Large Straight",
|
|
|
persona: "加急大型直卡",
|
|
|
input: {
|
|
|
...DEFAULT_EXPEDITED_DEMO,
|
|
|
expeditedTrailer: "large_straight",
|
|
|
weightLb: "10000",
|
|
|
},
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
{
|
|
|
id: "E04-expedited-liftgate",
|
|
|
title: "Expedited Large Straight + Liftgate",
|
|
|
persona: "加急大直卡需尾板",
|
|
|
input: {
|
|
|
...DEFAULT_EXPEDITED_DEMO,
|
|
|
expeditedTrailer: "large_straight",
|
|
|
expeditedLiftgate: true,
|
|
|
weightLb: "8000",
|
|
|
},
|
|
|
verified: "recorded",
|
|
|
},
|
|
|
];
|
|
|
|
|
|
/** 24 条 canonical 路径预期 RPA 终态(批量验收结论,供系统联调) */
|
|
|
export const PRIORITY1_CANONICAL_EXPECTED_OUTCOME: Record<
|
|
|
string,
|
|
|
"instant_quote" | "manual_followup"
|
|
|
> = {
|
|
|
"P01-ltl-residential": "instant_quote",
|
|
|
"P02-ltl-business-dock": "instant_quote",
|
|
|
"P03-ltl-business-no-dock": "instant_quote",
|
|
|
"P04-ltl-crate": "instant_quote",
|
|
|
"P05-ltl-box": "instant_quote",
|
|
|
"P06-ltl-bundle": "instant_quote",
|
|
|
"P07-ltl-not-sure-class": "instant_quote",
|
|
|
"P08-ltl-all-accessorials": "instant_quote",
|
|
|
"P09-ltl-frequent-shipper": "instant_quote",
|
|
|
"F01-ftl-dry-van-full": "instant_quote",
|
|
|
"F02-ftl-dry-van-partial": "instant_quote",
|
|
|
"F03-ftl-dry-van-team": "instant_quote",
|
|
|
"F04-ftl-open-deck-flatbed": "instant_quote",
|
|
|
"F05-ftl-open-deck-step": "instant_quote",
|
|
|
"F06-ftl-open-deck-conestoga": "manual_followup",
|
|
|
"F07-ftl-open-deck-rgn": "manual_followup",
|
|
|
"F08-ftl-temperature": "manual_followup",
|
|
|
"F09-ftl-expedited-sprinter": "manual_followup",
|
|
|
"F10-ftl-expedited-small": "manual_followup",
|
|
|
"F11-ftl-expedited-large-liftgate": "manual_followup",
|
|
|
"E01-expedited-sprinter": "manual_followup",
|
|
|
"E02-expedited-small": "manual_followup",
|
|
|
"E03-expedited-large": "manual_followup",
|
|
|
"E04-expedited-liftgate": "manual_followup",
|
|
|
};
|
|
|
|
|
|
export type Priority1CanonicalTestCase = {
|
|
|
index: number;
|
|
|
id: string;
|
|
|
title: string;
|
|
|
category: "LTL" | "FTL" | "Expedited";
|
|
|
persona: string;
|
|
|
verified: "recorded" | "logic-only";
|
|
|
expectedOutcome: "instant_quote" | "manual_followup";
|
|
|
expectedMessage: string | null;
|
|
|
filter: string;
|
|
|
apiPayload: {
|
|
|
request_id: string;
|
|
|
customer_id: string;
|
|
|
priority1_input: Priority1DemoInput;
|
|
|
};
|
|
|
};
|
|
|
|
|
|
const MANUAL_FOLLOWUP_MSG = "表单已提交,站点判定需人工确认细节后报价";
|
|
|
|
|
|
/** 生成 24 条系统联调用例(POST /api/priority1/quotes) */
|
|
|
export function buildPriority1CanonicalTestCases(opts: {
|
|
|
customerId?: string;
|
|
|
requestIdPrefix?: string;
|
|
|
} = {}): Priority1CanonicalTestCase[] {
|
|
|
const customerId = opts.customerId ?? "CUST_PRIORITY1_TEST";
|
|
|
const prefix = opts.requestIdPrefix ?? "p1-matrix";
|
|
|
return PRIORITY1_CANONICAL_PATHS.map((path, index) => {
|
|
|
const expectedOutcome =
|
|
|
PRIORITY1_CANONICAL_EXPECTED_OUTCOME[path.id] ?? "instant_quote";
|
|
|
return {
|
|
|
index: index + 1,
|
|
|
id: path.id,
|
|
|
title: path.title,
|
|
|
category: path.id.startsWith("P")
|
|
|
? "LTL"
|
|
|
: path.id.startsWith("F")
|
|
|
? "FTL"
|
|
|
: "Expedited",
|
|
|
persona: path.persona,
|
|
|
verified: path.verified,
|
|
|
expectedOutcome,
|
|
|
expectedMessage:
|
|
|
expectedOutcome === "manual_followup" ? MANUAL_FOLLOWUP_MSG : null,
|
|
|
filter: path.id.replace(/-.*$/, ""),
|
|
|
apiPayload: {
|
|
|
request_id: `${prefix}-${path.id}`,
|
|
|
customer_id: customerId,
|
|
|
priority1_input: path.input,
|
|
|
},
|
|
|
};
|
|
|
});
|
|
|
}
|
|
|
|
|
|
const ZIP_POOL = [
|
|
|
"10118", "90017", "60601", "77002", "33131", "98101", "80202", "19107",
|
|
|
"02108", "75201", "30303", "48226", "44114", "85004", "37203", "63101",
|
|
|
"97201", "89101", "32801", "28202",
|
|
|
];
|
|
|
|
|
|
const FREIGHT_CLASSES = ["50", "55", "60", "65", "70", "77.5", "85", "92.5", "100"];
|
|
|
const FTL_TRAILERS = Object.keys(FTL_TRAILER) as FtlTrailerType[];
|
|
|
const EXP_TRAILERS = Object.keys(EXPEDITED_TRAILER) as ExpeditedTrailerType[];
|
|
|
const COMMODITIES = [
|
|
|
"General Freight",
|
|
|
"Palletized Goods",
|
|
|
"Electronics",
|
|
|
"Machinery",
|
|
|
"Furniture",
|
|
|
"Food Products",
|
|
|
];
|
|
|
|
|
|
function mulberry32(seed: number): () => number {
|
|
|
let s = seed >>> 0;
|
|
|
return () => {
|
|
|
s = (s + 0x6d2b79f5) >>> 0;
|
|
|
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
|
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
|
};
|
|
|
}
|
|
|
|
|
|
function pick<T>(rng: () => number, arr: T[]): T {
|
|
|
return arr[Math.floor(rng() * arr.length)]!;
|
|
|
}
|
|
|
|
|
|
function randInt(rng: () => number, min: number, max: number): number {
|
|
|
return Math.floor(rng() * (max - min + 1)) + min;
|
|
|
}
|
|
|
|
|
|
function futurePickupDate(rng: () => number, base = new Date()): string {
|
|
|
const d = new Date(base);
|
|
|
d.setDate(d.getDate() + randInt(rng, 3, 21));
|
|
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
|
const dd = String(d.getDate()).padStart(2, "0");
|
|
|
return `${mm}/${dd}/${d.getFullYear()}`;
|
|
|
}
|
|
|
|
|
|
function randomUsPhone(rng: () => number): string {
|
|
|
const area = randInt(rng, 201, 989);
|
|
|
const exchange = randInt(rng, 201, 999);
|
|
|
const line = randInt(rng, 1000, 9999);
|
|
|
return `${area}${exchange}${line}`.slice(0, 10);
|
|
|
}
|
|
|
|
|
|
function randomZipPair(rng: () => number): { originZip: string; destinationZip: string } {
|
|
|
let originZip = pick(rng, ZIP_POOL);
|
|
|
let destinationZip = pick(rng, ZIP_POOL);
|
|
|
let guard = 0;
|
|
|
while (destinationZip === originZip && guard < 20) {
|
|
|
destinationZip = pick(rng, ZIP_POOL);
|
|
|
guard += 1;
|
|
|
}
|
|
|
return { originZip, destinationZip };
|
|
|
}
|
|
|
|
|
|
function baseForType(type: Priority1ShipmentType): Priority1DemoInput {
|
|
|
if (type === "ftl") return { ...DEFAULT_FTL_DEMO };
|
|
|
if (type === "expedited") return { ...DEFAULT_EXPEDITED_DEMO };
|
|
|
return { ...DEFAULT_PRIORITY1_DEMO };
|
|
|
}
|
|
|
|
|
|
export function parsePriority1DemoFromEnv(
|
|
|
raw: string | undefined,
|
|
|
): Priority1DemoInput {
|
|
|
if (!raw?.trim()) return DEFAULT_PRIORITY1_DEMO;
|
|
|
try {
|
|
|
const parsed = JSON.parse(raw) as Partial<Priority1DemoInput>;
|
|
|
const base = baseForType(parsed.shipmentType ?? "ltl");
|
|
|
return { ...base, ...parsed };
|
|
|
} catch {
|
|
|
return DEFAULT_PRIORITY1_DEMO;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export function generateRandomPriority1Cases(
|
|
|
count: number,
|
|
|
seed = Date.now(),
|
|
|
): Array<{ id: string; input: Priority1DemoInput }> {
|
|
|
const rng = mulberry32(seed);
|
|
|
const out: Array<{ id: string; input: Priority1DemoInput }> = [];
|
|
|
|
|
|
for (let i = 0; i < count; i += 1) {
|
|
|
const zips = randomZipPair(rng);
|
|
|
const length = randInt(rng, 36, 96);
|
|
|
const width = randInt(rng, 36, 48);
|
|
|
const height = randInt(rng, 36, 72);
|
|
|
|
|
|
out.push({
|
|
|
id: `P${String(i + 1).padStart(2, "0")}`,
|
|
|
input: {
|
|
|
...DEFAULT_PRIORITY1_DEMO,
|
|
|
...zips,
|
|
|
pickupDate: futurePickupDate(rng),
|
|
|
phone: randomUsPhone(rng),
|
|
|
lengthIn: String(length),
|
|
|
widthIn: String(width),
|
|
|
heightIn: String(height),
|
|
|
weightLb: String(randInt(rng, 150, 1800)),
|
|
|
palletCount: String(randInt(rng, 1, 5)),
|
|
|
freightClass: pick(rng, FREIGHT_CLASSES),
|
|
|
},
|
|
|
});
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
}
|
|
|
|
|
|
export function generateRandomFtlCases(
|
|
|
count: number,
|
|
|
seed = Date.now(),
|
|
|
): Array<{ id: string; input: Priority1DemoInput }> {
|
|
|
const rng = mulberry32(seed);
|
|
|
const out: Array<{ id: string; input: Priority1DemoInput }> = [];
|
|
|
|
|
|
for (let i = 0; i < count; i += 1) {
|
|
|
const zips = randomZipPair(rng);
|
|
|
const services: FtlAdditionalService[] = [];
|
|
|
if (rng() > 0.5) services.push("full_ftl");
|
|
|
if (rng() > 0.7) services.push("partial_ptl");
|
|
|
|
|
|
out.push({
|
|
|
id: `F${String(i + 1).padStart(2, "0")}`,
|
|
|
input: {
|
|
|
...DEFAULT_FTL_DEMO,
|
|
|
...zips,
|
|
|
pickupDate: futurePickupDate(rng),
|
|
|
phone: randomUsPhone(rng),
|
|
|
commodity: pick(rng, COMMODITIES),
|
|
|
weightLb: String(randInt(rng, 5000, 42000)),
|
|
|
palletCount: String(randInt(rng, 4, 26)),
|
|
|
ftlTrailer: pick(rng, FTL_TRAILERS),
|
|
|
ftlAdditionalServices: services,
|
|
|
},
|
|
|
});
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
}
|
|
|
|
|
|
export function generateRandomExpeditedCases(
|
|
|
count: number,
|
|
|
seed = Date.now(),
|
|
|
): Array<{ id: string; input: Priority1DemoInput }> {
|
|
|
const rng = mulberry32(seed + 17);
|
|
|
const out: Array<{ id: string; input: Priority1DemoInput }> = [];
|
|
|
|
|
|
for (let i = 0; i < count; i += 1) {
|
|
|
const zips = randomZipPair(rng);
|
|
|
out.push({
|
|
|
id: `E${String(i + 1).padStart(2, "0")}`,
|
|
|
input: {
|
|
|
...DEFAULT_EXPEDITED_DEMO,
|
|
|
...zips,
|
|
|
pickupDate: futurePickupDate(rng),
|
|
|
phone: randomUsPhone(rng),
|
|
|
commodity: pick(rng, COMMODITIES),
|
|
|
weightLb: String(randInt(rng, 200, 3500)),
|
|
|
expeditedTrailer: pick(rng, EXP_TRAILERS),
|
|
|
},
|
|
|
});
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
}
|
|
|
|
|
|
export function formatPriority1InputSummary(input: Priority1DemoInput): string {
|
|
|
const cfg = getShipmentConfig(input.shipmentType);
|
|
|
const lines = [
|
|
|
`Shipment Type: ${cfg.displayName}`,
|
|
|
`起运 ZIP: ${input.originZip}`,
|
|
|
`目的 ZIP: ${input.destinationZip}`,
|
|
|
`发货频次: ${input.shipmentFrequency}`,
|
|
|
`提货日: ${input.pickupDate}`,
|
|
|
`邮箱: ${input.email}`,
|
|
|
`电话: +1 ${input.phone}`,
|
|
|
];
|
|
|
|
|
|
if (input.shipmentType === "ltl") {
|
|
|
lines.push(
|
|
|
`尺寸: ${input.lengthIn} × ${input.widthIn} × ${input.heightIn} in`,
|
|
|
`总重量: ${input.weightLb} lb`,
|
|
|
`托盘数: ${input.palletCount}`,
|
|
|
`包装: ${input.packaging}`,
|
|
|
`货运等级 Freight Class: ${input.freightClass}`,
|
|
|
`提货地点类型: ${input.pickupLocation}`,
|
|
|
`派送地点类型: ${input.deliveryLocation}`,
|
|
|
`提货附加(勾选): ${input.pickupServices.join("、")}`,
|
|
|
`派送附加(勾选): ${input.deliveryServices.join("、")}`,
|
|
|
);
|
|
|
}
|
|
|
|
|
|
if (input.shipmentType === "ftl") {
|
|
|
lines.push(
|
|
|
`主拖车类型: ${FTL_TRAILER[input.ftlTrailer].label}`,
|
|
|
);
|
|
|
if (input.ftlTrailer === "open_deck") {
|
|
|
lines.push(`Open Deck 子类型: ${input.openDeckSubtype}`);
|
|
|
}
|
|
|
if (input.ftlTrailer === "temperature_controlled") {
|
|
|
lines.push(`温控: ${input.tempMinF}°F ~ ${input.tempMaxF}°F`);
|
|
|
}
|
|
|
if (input.ftlTrailer === "ftl_expedited") {
|
|
|
lines.push(
|
|
|
`Expedited 子车型: ${EXPEDITED_TRAILER[input.ftlExpeditedTrailer].label}`,
|
|
|
);
|
|
|
if (input.expeditedLiftgate && input.ftlExpeditedTrailer === "large_straight") {
|
|
|
lines.push("附加: Liftgate");
|
|
|
}
|
|
|
}
|
|
|
lines.push(
|
|
|
`附加服务: ${
|
|
|
input.ftlAdditionalServices.length
|
|
|
? input.ftlAdditionalServices
|
|
|
.map((s) => FTL_ADDITIONAL[s].label)
|
|
|
.join("、")
|
|
|
: "(无)"
|
|
|
}`,
|
|
|
`货描 Commodity: ${input.commodity}`,
|
|
|
`总重量: ${input.weightLb} lb`,
|
|
|
);
|
|
|
if (input.ftlTrailer !== "ftl_expedited") {
|
|
|
lines.push(`托盘数: ${input.palletCount}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (input.shipmentType === "expedited") {
|
|
|
lines.push(
|
|
|
`拖车类型: ${EXPEDITED_TRAILER[input.expeditedTrailer].label}`,
|
|
|
`货描 Commodity: ${input.commodity}`,
|
|
|
`总重量: ${input.weightLb} lb`,
|
|
|
);
|
|
|
if (input.expeditedLiftgate && input.expeditedTrailer === "large_straight") {
|
|
|
lines.push("附加: Liftgate");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return lines.join("\n");
|
|
|
}
|