Files
admin-web/src/modules/overseas-warehouse/dispatchWire.ts
T

89 lines
12 KiB
TypeScript

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<T, K extends keyof T> = Omit<T, K> & { [P in K]: LongQuantityWire }
type CountsWire = LongFields<Counts, keyof Counts>
type ReviewCountsWire = LongFields<ReviewCounts, keyof ReviewCounts>
type OutboundWire = LongFields<OutboundReference, 'packageCount' | 'palletCount'>
type DeliveryWire = LongFields<DeliveryReference, 'packageCount' | 'palletCount'>
type ReferencesWire = { outbounds: OutboundWire[]; deliveries: DeliveryWire[] }
type StatisticsWire = Omit<DispatchStatistics, 'matchedTotal' | 'activeTotal' | 'cancelledTotal' | 'stageCounts' | 'beforeLoadingReviewCounts'> & {
matchedTotal: LongQuantityWire; activeTotal: LongQuantityWire; cancelledTotal: LongQuantityWire
stageCounts: Record<DispatchStage, LongQuantityWire>
beforeLoadingReviewCounts: { vehicleInfoTotal: LongQuantityWire; dispatchReview: ReviewCountsWire; loadingReview: ReviewCountsWire }
}
type ListRowWire = Omit<DispatchListRow, 'counts'> & { counts: CountsWire }
export type DispatchPageWire = Omit<DispatchPage, 'records' | 'total' | 'page' | 'size' | 'pages' | 'statistics'> & { records: ListRowWire[]; total: LongQuantityWire; page: LongQuantityWire; size: LongQuantityWire; pages: LongQuantityWire; statistics: StatisticsWire }
export type DispatchDetailWire = Omit<DispatchDetail, 'counts' | 'documents'> & { counts: CountsWire; documents: Omit<DispatchDetail['documents'], keyof ReferencesWire> & ReferencesWire }
type SelectionGroupWire = Omit<SelectionGroup, 'sources' | 'pallets'> & { sources: Array<LongFields<SelectionGroup['sources'][number], 'packages'>>; pallets: Array<LongFields<SelectionGroup['pallets'][number], 'packageCount'>> }
export type CandidateWire = Omit<LongFields<Candidate, 'availablePackages'>, 'selectionGroup'> & { selectionGroup: SelectionGroupWire }
export type CandidateListWire = Omit<LongFields<CandidateList, 'total' | 'pages'>, 'candidates'> & { candidates: CandidateWire[] }
export type DispatchDraftWire = Omit<DispatchDraft, 'sources'> & { sources: CandidateWire[] }
export type DispatchItemsWire = Omit<DispatchItems, 'items'> & { items: Array<LongFields<DispatchItems['items'][number], 'scheduledPackages' | 'pickedPackages'>> }
export type LoadingPreviewWire = Omit<LoadingPreview, 'pallets'> & { pallets: Array<LongFields<LoadingPreview['pallets'][number], 'scheduledPackages' | 'pickedPackages'>> }
export type DispatchEvidenceFileWire = LongFields<DispatchEvidenceFile, 'sizeBytes'>
type ArrangementResultWire = Omit<ArrangementResult, keyof ReferencesWire> & ReferencesWire
export type SourceOriginWire = Omit<SourceOrigin, 'productQuantity'> & { productQuantity: LongQuantityWire | null }
type CargoLineWire = Omit<LongFields<CargoLine, 'packages' | 'productQuantity'>, 'sourceOrigin'> & { sourceOrigin: SourceOriginWire | null }
type PreviewGroupWire = Omit<PreviewGroup, 'cargo'> & { cargo: CargoLineWire[] }
export type ArrangementPreviewWire = Omit<ArrangementPreview, 'groups' | 'arrangement'> & { groups: PreviewGroupWire[]; arrangement: ArrangementResultWire | null }
type PickedWire = LongFields<Extract<CommandResult, { action: 'PICK' }>['result'], 'packages' | 'pickedPackages'>
type DepartedWire = Omit<Extract<CommandResult, { action: 'DEPART' }>['result'], 'shippedPackages' | keyof ReferencesWire> & ReferencesWire & { shippedPackages: LongQuantityWire }
type WireCommand<C extends CommandResult> = C extends CommandResult ? Omit<C, 'counts' | 'result'> & { 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<CommandResult>
export type ConvertedDraftWire = Omit<ConvertedDraft, 'vehicles'> & { 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<T, K extends keyof T>(value: LongFields<T, K>, 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<Counts, keyof Counts>(value, ['itemCount', 'palletCount', 'packageCount', 'pickedPackageCount', 'shippedPackageCount'])
const reviewCounts = (value: ReviewCountsWire) => numbers<ReviewCounts, keyof ReviewCounts>(value, ['notSubmitted', 'pending', 'approved', 'rejected'])
const references = (value: ReferencesWire) => ({ outbounds: value.outbounds.map(row => numbers<OutboundReference, 'packageCount' | 'palletCount'>(row, ['packageCount', 'palletCount'])), deliveries: value.deliveries.map(row => numbers<DeliveryReference, 'packageCount' | 'palletCount'>(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<DispatchStage, number>, 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) })) } }