import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' import type { FbaAppointmentDto } from '@/modules/overseas-warehouse' export interface BatchSourceAllocation { customerId: string; sourceKey: string; destinationKey: string; packageCount: number | null originalPackages: number | null; sourceVersion: string | null; cargoVerificationVersion: number | null; shipmentReferences: string[] } export interface FbaAppointmentBatchContext { appointment: FbaAppointmentDto; originalSourceRefs: Array<{ customerId: string; sourceKey: string; destinationKey: string }>; currentBatches: FbaAppointmentDto[]; sources: BatchSourceAllocation[]; allowedActions: Array<'VERIFY_CARGO' | 'SPLIT' | 'MERGE'>; blockReasons: string[] } export interface BatchAllocation { customerId: string; sourceKey: string; packageCount: number; expectedSourceVersion: string | null; expectedVerificationVersion: number | null } export interface RepartitionFbaAppointmentsRequest { originals: Array<{ appointmentNo: string; version: number }> batches: Array<{ plannedDate: string | null; remark: string | null; allocations: BatchAllocation[] }> reason: string; idempotencyKey: string } export interface RepartitionFbaAppointmentsResponse { replacedBatchKeys: string[]; replacedAppointmentNos: string[]; appointments: FbaAppointmentDto[] } const base = '/platform/v1/last-leg/appointment-planning' export async function getAppointmentBatch(batchKey: string) { return unwrapApiResponse(await http.get, ApiEnvelope>(`${base}/batches/${encodeURIComponent(batchKey)}`)) } export async function repartitionAppointments(request: RepartitionFbaAppointmentsRequest) { return unwrapApiResponse(await http.post, ApiEnvelope>(`${base}/appointments/repartition`, request)) } export const allocationKey = (source: Pick) => JSON.stringify([source.customerId, source.sourceKey, source.destinationKey]) export function mergeBatchSources(contexts: FbaAppointmentBatchContext[]): BatchSourceAllocation[] { const first = contexts[0] if (!first) throw new Error('请先选择批次') const rows = new Map() const seen = new Set() for (const context of contexts) { const appointment = context.appointment if (seen.has(appointment.appointmentNo)) throw new Error('同一批次不能重复选择') seen.add(appointment.appointmentNo) if (appointment.originWarehouseId !== first.appointment.originWarehouseId || appointment.fbaWarehouseCode !== first.appointment.fbaWarehouseCode) throw new Error('只允许同一起运仓、同一目的仓的批次合并') if (appointment.replacementBatchKeys?.length || appointment.replacementAppointmentNos?.length) throw new Error('原批次已被替代,请选择当前有效批次') if (!context.sources.length) throw new Error('批次缺少真实货物来源,请核对后再操作') for (const source of context.sources) { if (source.packageCount == null || !Number.isSafeInteger(Number(source.packageCount)) || Number(source.packageCount) < 1) throw new Error('请先核实每个来源的本批箱数') const key = allocationKey(source), previous = rows.get(key) if (previous && (previous.sourceVersion !== source.sourceVersion || previous.cargoVerificationVersion !== source.cargoVerificationVersion || JSON.stringify(previous.shipmentReferences) !== JSON.stringify(source.shipmentReferences))) throw new Error('同一来源的核实版本或货件资料不一致,请重新读取批次') const count = Number(source.packageCount) + (previous?.packageCount || 0) if (!Number.isSafeInteger(count)) throw new Error('来源箱数超出可处理范围') rows.set(key, { ...source, packageCount: count }) } } return [...rows.values()] } export function allocation(source: BatchSourceAllocation, packages: number): BatchAllocation { return { customerId: source.customerId, sourceKey: source.sourceKey, packageCount: packages, expectedSourceVersion: source.sourceVersion, expectedVerificationVersion: source.cargoVerificationVersion } } export function splitBatch(context: FbaAppointmentBatchContext, moved: Record): [BatchAllocation[], BatchAllocation[]] { const remaining: BatchAllocation[] = [], next: BatchAllocation[] = [] for (const source of mergeBatchSources([context])) { const quantity = Number(moved[allocationKey(source)] ?? 0), total = Number(source.packageCount) if (!Number.isSafeInteger(quantity) || quantity < 0 || quantity > total) throw new Error('每票分出箱数须为0至本批箱数之间的整数') if (quantity > 0) next.push(allocation(source, quantity)) if (quantity < total) remaining.push(allocation(source, total - quantity)) } if (!remaining.length || !next.length) throw new Error('两批都须保留至少一箱货物') return [remaining, next] }