|
|
/**
|
|
|
* MotherShip 登录态附加服务组合约束
|
|
|
* 依据:官网门户拒价文案(住宅提货)、API accessorial 语义、实测易无价组合
|
|
|
*/
|
|
|
|
|
|
export type MsCompatIssue = {
|
|
|
severity: "block" | "warn";
|
|
|
code: string;
|
|
|
message: string;
|
|
|
};
|
|
|
|
|
|
/** 提货侧不可选的附加服务(官网承运商拒价) */
|
|
|
export const MS_PICKUP_BLOCKED_ACCESSORIALS: Readonly<
|
|
|
Record<string, string>
|
|
|
> = {
|
|
|
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");
|
|
|
}
|