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.
chajia/lib/flock/flock-checkout-selection.ts

237 lines
7.4 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.

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