import { DispatchInputError, requireBatchVersions, type ArrangementPreview, type ArrangementResult, type Candidate, type CandidateList, type CargoLine, type CommandResult, type ConvertedDraft, type Counts, type DeliveryReference, type DispatchDetail, type DispatchDraft, type DispatchEvidenceFile, type DispatchItems, type DispatchListRow, type DispatchPage, type DispatchStage, type DispatchStatistics, type LoadingPreview, type OutboundReference, type PreviewGroup, type ReviewCounts, type SelectionGroup, type SourceOrigin } from './dispatchContracts' // common-web serializes both Long and long as decimal strings. Only quantity // fields are normalized here; IDs, recordVersion and reviewRound stay lossless. export type LongQuantityWire = string | number type LongFields = Omit & { [P in K]: LongQuantityWire } type CountsWire = LongFields type ReviewCountsWire = LongFields type OutboundWire = LongFields type DeliveryWire = LongFields type ReferencesWire = { outbounds: OutboundWire[]; deliveries: DeliveryWire[] } type StatisticsWire = Omit & { matchedTotal: LongQuantityWire; activeTotal: LongQuantityWire; cancelledTotal: LongQuantityWire stageCounts: Record beforeLoadingReviewCounts: { vehicleInfoTotal: LongQuantityWire; dispatchReview: ReviewCountsWire; loadingReview: ReviewCountsWire } } type ListRowWire = Omit & { counts: CountsWire } export type DispatchPageWire = Omit & { records: ListRowWire[]; total: LongQuantityWire; page: LongQuantityWire; size: LongQuantityWire; pages: LongQuantityWire; statistics: StatisticsWire } export type DispatchDetailWire = Omit & { counts: CountsWire; documents: Omit & ReferencesWire } type SelectionGroupWire = Omit & { sources: Array>; pallets: Array> } export type CandidateWire = Omit, 'selectionGroup'> & { selectionGroup: SelectionGroupWire } export type CandidateListWire = Omit, 'candidates'> & { candidates: CandidateWire[] } export type DispatchDraftWire = Omit & { sources: CandidateWire[] } export type DispatchItemsWire = Omit & { items: Array> } export type LoadingPreviewWire = Omit & { pallets: Array> } export type DispatchEvidenceFileWire = LongFields type ArrangementResultWire = Omit & ReferencesWire export type SourceOriginWire = Omit & { productQuantity: LongQuantityWire | null } type CargoLineWire = Omit, 'sourceOrigin'> & { sourceOrigin: SourceOriginWire | null } type PreviewGroupWire = Omit & { cargo: CargoLineWire[] } export type ArrangementPreviewWire = Omit & { groups: PreviewGroupWire[]; arrangement: ArrangementResultWire | null } type PickedWire = LongFields['result'], 'packages' | 'pickedPackages'> type DepartedWire = Omit['result'], 'shippedPackages' | keyof ReferencesWire> & ReferencesWire & { shippedPackages: LongQuantityWire } type WireCommand = C extends CommandResult ? Omit & { counts: CountsWire; result: C extends { action: 'PICK' } ? PickedWire : C extends { action: 'DEPART' } ? DepartedWire : C extends { action: 'ARRANGE_DELIVERY' } ? ArrangementResultWire : C['result'] } : never export type CommandResultWire = WireCommand export type ConvertedDraftWire = Omit & { vehicles: Array<{ draftContainerId: string; dispatch: CommandResultWire }> } export function dispatchQuantity(value: unknown, field: string): number { if (typeof value !== 'number' && (typeof value !== 'string' || !/^(?:0|[1-9]\d*)$/.test(value))) throw new DispatchInputError(`排车响应${field}不是有效非负整数,请重新读取`) const number = typeof value === 'number' ? value : Number(value) if (!Number.isSafeInteger(number) || number < 0) throw new DispatchInputError(`排车响应${field}超出安全整数范围或无效,请重新读取`) return number } function numbers(value: LongFields, fields: readonly K[]): T { const result = { ...value } for (const field of fields) Object.assign(result, { [field]: dispatchQuantity(value[field], String(field)) }) return result as T } const counts = (value: CountsWire) => numbers(value, ['itemCount', 'palletCount', 'packageCount', 'pickedPackageCount', 'shippedPackageCount']) const reviewCounts = (value: ReviewCountsWire) => numbers(value, ['notSubmitted', 'pending', 'approved', 'rejected']) const references = (value: ReferencesWire) => ({ outbounds: value.outbounds.map(row => numbers(row, ['packageCount', 'palletCount'])), deliveries: value.deliveries.map(row => numbers(row, ['packageCount', 'palletCount'])) }) const candidate = (value: CandidateWire): Candidate => { requireBatchVersions(value.appointmentBatches) const group = value.selectionGroup if (!group || typeof group.groupKey !== 'string' || typeof group.groupVersion !== 'string' || !Array.isArray(group.sources) || !Array.isArray(group.pallets) || !Array.isArray(group.blockReasons)) throw new DispatchInputError('完整选货组或组成版本缺失,请重新读取候选') return { ...value, availablePackages: dispatchQuantity(value.availablePackages, 'availablePackages'), selectionGroup: { ...group, sources: group.sources.map(source => ({ ...source, packages: dispatchQuantity(source.packages, 'selectionGroup.sources.packages') })), pallets: group.pallets.map(pallet => ({ ...pallet, packageCount: dispatchQuantity(pallet.packageCount, 'selectionGroup.pallets.packageCount') })) } } } function statistics(value: StatisticsWire): DispatchStatistics { return { ...value, matchedTotal: dispatchQuantity(value.matchedTotal, 'matchedTotal'), activeTotal: dispatchQuantity(value.activeTotal, 'activeTotal'), cancelledTotal: dispatchQuantity(value.cancelledTotal, 'cancelledTotal'), stageCounts: Object.fromEntries(Object.entries(value.stageCounts).map(([stage, count]) => [stage, dispatchQuantity(count, `stageCounts.${stage}`)])) as Record, beforeLoadingReviewCounts: { vehicleInfoTotal: dispatchQuantity(value.beforeLoadingReviewCounts.vehicleInfoTotal, 'vehicleInfoTotal'), dispatchReview: reviewCounts(value.beforeLoadingReviewCounts.dispatchReview), loadingReview: reviewCounts(value.beforeLoadingReviewCounts.loadingReview) } } } export function normalizeDispatchPage(value: DispatchPageWire): DispatchPage { return { ...value, total: dispatchQuantity(value.total, 'total'), page: dispatchQuantity(value.page, 'page'), size: dispatchQuantity(value.size, 'size'), pages: dispatchQuantity(value.pages, 'pages'), records: value.records.map(row => ({ ...row, counts: counts(row.counts) })), statistics: statistics(value.statistics) } } export function normalizeDispatchDetail(value: DispatchDetailWire): DispatchDetail { requireBatchVersions(value.dispatchInfo.appointmentBatches); return { ...value, counts: counts(value.counts), documents: { ...value.documents, ...references(value.documents) } } } export function normalizeCandidateList(value: CandidateListWire): CandidateList { const result = { ...value, candidates: value.candidates.map(candidate), total: dispatchQuantity(value.total, 'total'), page: dispatchQuantity(value.page, 'page'), size: dispatchQuantity(value.size, 'size'), pages: dispatchQuantity(value.pages, 'pages') } if (result.page < 1 || result.size < 1 || result.size > 50 || result.pages !== Math.ceil(result.total / result.size) || result.candidates.length > result.size) throw new DispatchInputError('候选分页信息无效,请重新读取') return result } export function normalizeDispatchDraft(value: DispatchDraftWire): DispatchDraft { return { ...value, sources: value.sources.map(candidate) } } export function normalizeDispatchItems(value: DispatchItemsWire): DispatchItems { return { ...value, items: value.items.map(row => ({ ...row, scheduledPackages: dispatchQuantity(row.scheduledPackages, 'scheduledPackages'), pickedPackages: dispatchQuantity(row.pickedPackages, 'pickedPackages') })) } } export function normalizeLoadingPreview(value: LoadingPreviewWire): LoadingPreview { return { ...value, pallets: value.pallets.map(row => ({ ...row, scheduledPackages: dispatchQuantity(row.scheduledPackages, 'scheduledPackages'), pickedPackages: dispatchQuantity(row.pickedPackages, 'pickedPackages') })) } } export function normalizeEvidenceFile(value: DispatchEvidenceFileWire): DispatchEvidenceFile { return { ...value, sizeBytes: dispatchQuantity(value.sizeBytes, 'sizeBytes') } } function arrangementResult(value: ArrangementResultWire): ArrangementResult { return { ...value, ...references(value) } } function normalizeSourceOrigin(value: SourceOriginWire | null): SourceOrigin | null { if (value == null) return null if (!value || typeof value !== 'object' || Array.isArray(value)) throw new DispatchInputError('排车响应原货源结构缺失,请重新读取') return { ...value, productQuantity: value.productQuantity == null ? null : dispatchQuantity(value.productQuantity, 'sourceOrigin.productQuantity') } } export function normalizeArrangementPreview(value: ArrangementPreviewWire): ArrangementPreview { return { ...value, groups: value.groups.map(group => ({ ...group, cargo: group.cargo.map(row => ({ ...row, packages: dispatchQuantity(row.packages, 'packages'), productQuantity: dispatchQuantity(row.productQuantity, 'productQuantity'), sourceOrigin: normalizeSourceOrigin(row.sourceOrigin) })) })), arrangement: value.arrangement ? arrangementResult(value.arrangement) : null } } export function normalizeDispatchCommand(value: CommandResultWire): CommandResult { const normalizedCounts = counts(value.counts) switch (value.action) { case 'PICK': return { ...value, counts: normalizedCounts, result: { ...value.result, packages: dispatchQuantity(value.result.packages, 'packages'), pickedPackages: dispatchQuantity(value.result.pickedPackages, 'pickedPackages') } } case 'DEPART': return { ...value, counts: normalizedCounts, result: { ...value.result, shippedPackages: dispatchQuantity(value.result.shippedPackages, 'shippedPackages'), ...references(value.result) } } case 'ARRANGE_DELIVERY': return { ...value, counts: normalizedCounts, result: arrangementResult(value.result) } default: return { ...value, counts: normalizedCounts } } } export function normalizeConvertedDraft(value: ConvertedDraftWire): ConvertedDraft { return { ...value, vehicles: value.vehicles.map(row => ({ ...row, dispatch: normalizeDispatchCommand(row.dispatch) })) } }