/** * MotherShip 登录态附加服务组合约束 * 依据:官网门户拒价文案(住宅提货)、API accessorial 语义、实测易无价组合 */ export type MsCompatIssue = { severity: "block" | "warn"; code: string; message: string; }; /** 提货侧不可选的附加服务(官网承运商拒价) */ export const MS_PICKUP_BLOCKED_ACCESSORIALS: Readonly< Record > = { residential: "承运商不支持住宅地址上门取件,请改选商业提货地址,或取消「住宅」", }; /** 同侧互斥:勾选后者会取消前者并提示 */ export const MS_ACCESSORIAL_EXCLUSIVE_PAIRS: ReadonlyArray<{ a: string; b: string; message: string; }> = [ { a: "appointment", b: "fbaAppointment", message: "「预约」与「Amazon 预约」不能同时选择,请只保留一项", }, { a: "residential", b: "tradeshow", message: "「住宅」与「展会」不宜同时勾选,请按实际地点类型只选一项", }, { a: "cfs", b: "residential", message: "CFS 与「住宅」不能同时选择,请按实际地点只选一项", }, ]; export function evaluateMothershipAccessorialCompat(input: { side: "pickup" | "delivery"; selected: readonly string[]; }): MsCompatIssue[] { const selected = [...input.selected]; const issues: MsCompatIssue[] = []; if (input.side === "pickup" && selected.includes("residential")) { issues.push({ severity: "block", code: "ms_pickup_residential", message: MS_PICKUP_BLOCKED_ACCESSORIALS.residential!, }); } for (const pair of MS_ACCESSORIAL_EXCLUSIVE_PAIRS) { if (selected.includes(pair.a) && selected.includes(pair.b)) { // 预约互斥仅派送侧有这两项 if ( (pair.a === "appointment" || pair.b === "appointment") && input.side === "pickup" ) { continue; } issues.push({ severity: "block", code: `ms_exclusive_${pair.a}_${pair.b}`, message: pair.message, }); } } if ( input.side === "delivery" && selected.includes("residential") && !selected.includes("liftgate") ) { issues.push({ severity: "warn", code: "ms_residential_need_liftgate", message: "住宅派送通常需要尾板,建议同时勾选「尾板(Liftgate)」以免追收", }); } if ( selected.includes("limitedAccess") && !selected.includes("liftgate") && !selected.includes("inside") ) { issues.push({ severity: "warn", code: "ms_limited_suggest_liftgate", message: "受限通道地点常需尾板或室内服务,建议按现场条件勾选以免无法卸货", }); } return issues; } /** * 尝试勾选/取消某附加服务。 * - 提货「住宅」:禁止勾选 * - 互斥对:勾选新项时自动去掉旧项 */ export function toggleMothershipAccessorial(input: { side: "pickup" | "delivery"; selected: readonly string[]; id: string; }): { next: string[]; applied: boolean; message?: string; issues: MsCompatIssue[]; } { const { side, id } = input; const selected = [...input.selected]; const currentlyOn = selected.includes(id); if (currentlyOn) { const next = selected.filter((x) => x !== id); return { next, applied: true, issues: evaluateMothershipAccessorialCompat({ side, selected: next }), }; } if (side === "pickup" && id === "residential") { return { next: selected, applied: false, message: MS_PICKUP_BLOCKED_ACCESSORIALS.residential, issues: evaluateMothershipAccessorialCompat({ side, selected }), }; } let next = [...selected, id]; let message: string | undefined; for (const pair of MS_ACCESSORIAL_EXCLUSIVE_PAIRS) { if (id !== pair.a && id !== pair.b) continue; const other = id === pair.a ? pair.b : pair.a; if (next.includes(other)) { next = next.filter((x) => x !== other); message = pair.message; } } return { next, applied: true, message, issues: evaluateMothershipAccessorialCompat({ side, selected: next }), }; } export function mothershipAccessorialHasBlock( issues: readonly MsCompatIssue[], ): boolean { return issues.some((i) => i.severity === "block"); }