/** * Flock 结账选档校验 + 档内灵活性价 / Standard 承运商列表 */ export type FlockCheckoutTier = "flock_direct" | "standard"; /** 官网 See Pricing Options 后三档(FlockDirect) */ export type FlockFlexibilityKey = "2_day" | "1_day" | "none"; /** 报价形态:灵活价 vs Standard LTL 承运商列表 */ export type FlockPricingOptionsType = "flexibility" | "carriers"; export type FlockCheckoutSelection = { preferredTier: FlockCheckoutTier; preferredFlexibility?: FlockFlexibilityKey | null; /** Standard LTL 承运商名(与 preferredFlexibility 二选一) */ preferredCarrier?: string | null; }; export type FlockFlexibilityOption = { key: FlockFlexibilityKey; /** 行内可见文案(flexibility / pickup / deliver / rate) */ label: string; rateUsd: number; pickupWindow?: string; deliverBy?: string; }; /** Standard LTL:See Pricing 后承运商行 */ export type FlockCarrierOption = { carrierName: string; transitDays: string; pickupDate?: string; deliveryEstimate?: string; rateUsd: number; label: string; }; /** 单档刮取结果(灵活价或承运商) */ export type FlockTierPricingScrape = { options_type: FlockPricingOptionsType; options: FlockFlexibilityOption[]; carrier_options: FlockCarrierOption[]; }; export function parseFlockFlexibilityKey( label: string, ): FlockFlexibilityKey | null { const t = label.replace(/\s+/g, " ").toLowerCase(); if (/2[-\s]?day\s+flexibility/.test(t)) return "2_day"; if (/1[-\s]?day\s+flexibility/.test(t)) return "1_day"; if (/no\s+flexibility/.test(t)) return "none"; return null; } export function flockFlexibilityKeyLabel(key: FlockFlexibilityKey): string { switch (key) { case "2_day": return "2 天灵活性"; case "1_day": return "1 天灵活性"; case "none": return "无灵活性"; default: return key; } } /** 返回中文错误;null = 通过 */ export function validateFlockCheckoutSelection( input: FlockCheckoutSelection, ): string | null { const tier = input.preferredTier; if (tier !== "flock_direct" && tier !== "standard") { return "请选择 FlockDirect® 或 Standard 报价档"; } const carrier = input.preferredCarrier?.trim() || ""; const hasFlex = input.preferredFlexibility === "2_day" || input.preferredFlexibility === "1_day" || input.preferredFlexibility === "none"; if (carrier && hasFlex) { return "灵活性档与承运商不能同时选择"; } if (carrier) { if (carrier.length > 128) return "承运商名称过长"; return null; } if (!hasFlex) { return "请选择灵活性报价档或承运商"; } return null; } /** 承运商列表选最低价;同价取首项;空列表返回 -1 */ export function pickLowestCarrierOptionIndex( options: FlockCarrierOption[], ): number { if (options.length === 0) return -1; let best = 0; for (let i = 1; i < options.length; i += 1) { if (options[i]!.rateUsd < options[best]!.rateUsd) { best = i; } } return best; } /** 按承运商名选;找不到回落最低价 */ export function pickCarrierOptionIndex( options: FlockCarrierOption[], carrierName: string | null | undefined, ): number { const want = carrierName?.trim().toLowerCase(); if (!want) return pickLowestCarrierOptionIndex(options); const idx = options.findIndex( (o) => o.carrierName.trim().toLowerCase() === want, ); return idx >= 0 ? idx : pickLowestCarrierOptionIndex(options); } /** 从官网承运商行文案提炼字段 */ export function enrichCarrierOption(raw: { label: string; rateUsd: number; }): FlockCarrierOption | null { const text = raw.label.replace(/\s+/g, " ").trim(); if (!text || /Save on average|Don't ship air|Quote shipment again/i.test(text)) { return null; } const transit = text.match(/(\d+(?:\s*[–\-]\s*\d+)?)\s*business\s*days?/i)?.[0]?.trim() || text.match(/(\d+)\s*days?/i)?.[0]?.trim() || ""; const dateRange = text.match( /((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2}(?:\s*[–\-]\s*(?:(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+)?\d{1,2})?)/i, )?.[1]?.trim() || undefined; let pickupDate: string | undefined; let deliveryEstimate: string | undefined; if (dateRange) { const parts = dateRange.split(/\s*[–\-]\s*/); pickupDate = parts[0]?.trim(); deliveryEstimate = parts[1]?.trim() || parts[0]?.trim(); } // 承运商名:去掉 Select / 价格 / 时效 / 日期 / 表头噪声后的剩余前缀 let carrierName = text .replace(/Select\s*→?/gi, "") .replace(/\$\s*[\d,]+(?:\.\d{1,2})?/g, "") .replace(/\d+(?:\s*[–\-]\s*\d+)?\s*business\s*days?/gi, "") .replace( /(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2}(?:\s*[–\-]\s*(?:(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+)?\d{1,2})?/gi, "", ) .replace( /\b(Carrier|Transit|from carrier|Pickup|Delivery|estimate|Rate|Select your preferred LTL carrier|Show predictive)\b/gi, "", ) .replace(/\s+/g, " ") .trim(); if (!carrierName || carrierName.length < 2) return null; if (carrierName.length > 80) carrierName = carrierName.slice(0, 80).trim(); // 行级祖先偶发吞整表:取最后一个像样的专名片段 if (/Inc\.?|LLC|Systems|Freight|Air|Lines/i.test(carrierName) && carrierName.length > 40) { const parts = carrierName.split(/\s{2,}|\s*\|\s*/).map((p) => p.trim()).filter(Boolean); const last = parts[parts.length - 1]; if (last && last.length >= 3 && last.length <= 60) carrierName = last; } return { carrierName, transitDays: transit || "—", pickupDate, deliveryEstimate, rateUsd: raw.rateUsd, label: text.slice(0, 200), }; } /** 档内选最低价;同价取首项;空列表返回 -1 */ export function pickLowestRateOptionIndex( options: FlockFlexibilityOption[], ): number { if (options.length === 0) return -1; let best = 0; for (let i = 1; i < options.length; i += 1) { if (options[i]!.rateUsd < options[best]!.rateUsd) { best = i; } } return best; } /** 按 key 选档;找不到返回 -1 */ export function pickFlexibilityOptionIndex( options: FlockFlexibilityOption[], key: FlockFlexibilityKey | null | undefined, ): number { if (!key) return pickLowestRateOptionIndex(options); const idx = options.findIndex((o) => o.key === key); return idx >= 0 ? idx : pickLowestRateOptionIndex(options); } export function parseFlockRateUsd(text: string): number | null { const m = text.replace(/,/g, "").match(/\$\s*([\d]+(?:\.\d{1,2})?)/); if (!m) return null; const n = Number(m[1]); return Number.isFinite(n) && n > 0 ? n : null; } /** 从官网行文案提炼 pickup / deliver */ export function parseFlexibilityWindows(label: string): { pickupWindow?: string; deliverBy?: string; } { const pickup = label.match(/Pickup\s+([A-Za-z0-9–\-\s,]+?)(?:\s+Deliver|$)/i)?.[1]?.trim() || undefined; const deliverBy = label.match(/Deliver\s+by\s+([A-Za-z0-9–\-\s,]+?)(?:\s+\$|$)/i)?.[1]?.trim() || undefined; return { pickupWindow: pickup, deliverBy }; } export function enrichFlexibilityOption( raw: { label: string; rateUsd: number }, ): FlockFlexibilityOption | null { const key = parseFlockFlexibilityKey(raw.label); if (!key) return null; const windows = parseFlexibilityWindows(raw.label); return { key, label: raw.label, rateUsd: raw.rateUsd, ...windows, }; }