import { isAxiosError } from 'axios' import { ApiResponseError } from '@/shared/api/http' import type { ProcurementCommand, ProcurementCommandBody } from './api/procurementCommands' export interface PendingProcurement { command: ProcurementCommand; payload: ProcurementCommandBody } const commands = new Set(['review', 'purchase', 'purchase/confirm', 'budget-authorizations', 'planned-warehouse', 'cancel', 'overseas-receipts', 'domestic-receipts', 'overseas-receipts/complete', 'domestic-receipts/complete', 'packing', 'packing/confirm', 'packing/withdraw', 'dispositions', 'receipts/correct', 'dispositions/revoke', 'labels', 'international-estimate', 'labels/apply', 'domestic-repack', 'overseas-shipment', 'customer-shipment', 'charges/confirm', 'fees/retry', 'fees/terminate', 'fees/release-estimate', 'fees/correct-target', 'fees/refund', 'fees/replace-failed', 'fees/additional', 'control-notes', 'control-notes/resolve', 'actual-warehouse/correct', 'exception-close']) export function commandKey() { const bytes = new Uint8Array(16); globalThis.crypto.getRandomValues(bytes) return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('') } export function readPendingProcurement(raw: string | null): PendingProcurement | null { if (raw === null) return null const value: unknown = JSON.parse(raw) if (!value || typeof value !== 'object' || !('command' in value) || !('payload' in value) || !commands.has(String(value.command))) throw new Error('invalid pending command') const payload = value.payload if (!payload || typeof payload !== 'object' || !('version' in payload) || !Number.isSafeInteger(payload.version) || Number(payload.version) < 0 || !('idempotencyKey' in payload) || typeof payload.idempotencyKey !== 'string' || !payload.idempotencyKey) throw new Error('invalid pending payload') const fields = new Set(['version', 'idempotencyKey', 'reason', 'decision', 'remark', 'purchaseWarehouseId', 'warehouseId', 'productName', 'specification', 'quantity', 'amount', 'currency', 'purchasedAt', 'estimatedArrivalAt', 'voucherFileId', 'leg', 'sourceCartonId', 'receiptId', 'dispositionId', 'goodQuantity', 'damagedQuantity', 'extraQuantity', 'receivedAt', 'kind', 'packingVersion', 'cartons', 'labelVersion', 'receiver', 'addressVoucherFileId', 'labels', 'cartonIds', 'businessDate', 'appliedAt', 'shippedAt', 'cartonId', 'lengthCm', 'widthCm', 'heightCm', 'grossWeightKg', 'feeId', 'targetOriginalAmount', 'internationalFreight', 'duty', 'domesticFreight', 'otherDetermination', 'otherFees', 'targetType', 'targetId', 'blocking', 'clientSummary', 'noteId', 'resolutions', 'customerVoucherFileId', 'financialConclusion', 'name']) const body = payload as Record if (Object.keys(body).some(field => !fields.has(field))) throw new Error('unexpected pending field') const nonempty = (field: string) => typeof body[field] === 'string' && !!body[field].trim() const positiveId = (field: string) => nonempty(field) && /^[1-9]\d*$/.test(String(body[field])) if (value.command === 'review') { if (!['APPROVE', 'REJECT'].includes(String(body.decision)) || (body.decision === 'APPROVE' && !positiveId('purchaseWarehouseId'))) throw new Error('invalid pending review') } else if (!nonempty('reason')) throw new Error('missing pending reason') if (value.command === 'planned-warehouse' && !positiveId('warehouseId')) throw new Error('invalid pending warehouse') if (['purchase', 'budget-authorizations'].includes(String(value.command))) { if (!/^\d{1,16}(?:\.\d{1,2})?$/.test(String(body.amount)) || !['CNY', 'USD'].includes(String(body.currency)) || !positiveId('voucherFileId')) throw new Error('invalid pending amount') } if (value.command === 'purchase' && (!nonempty('productName') || !Number.isSafeInteger(body.quantity) || Number(body.quantity) < 1 || !nonempty('purchasedAt') || !nonempty('estimatedArrivalAt'))) throw new Error('invalid pending purchase') const count = (field: string, minimum: number) => Number.isSafeInteger(body[field]) && Number(body[field]) >= minimum if (['overseas-receipts', 'domestic-receipts', 'overseas-receipts/complete', 'domestic-receipts/complete', 'receipts/correct', 'dispositions'].includes(String(value.command))) { if (!['OVERSEAS', 'DOMESTIC'].includes(String(body.leg))) throw new Error('invalid receipt leg') if ((String(value.command).startsWith('overseas') && body.leg !== 'OVERSEAS') || (String(value.command).startsWith('domestic') && body.leg !== 'DOMESTIC')) throw new Error('wrong receipt endpoint') } if (['overseas-receipts', 'domestic-receipts', 'receipts/correct'].includes(String(value.command))) { if (!['goodQuantity', 'damagedQuantity', 'extraQuantity'].every(field => count(field, 0)) || Number(body.goodQuantity) + Number(body.damagedQuantity) + Number(body.extraQuantity) <= 0 || !nonempty('receivedAt')) throw new Error('invalid pending receipt') if (body.leg === 'OVERSEAS' ? !positiveId('warehouseId') || body.sourceCartonId != null : !positiveId('sourceCartonId') || body.warehouseId != null) throw new Error('invalid receipt source') } if (['receipts/correct', 'dispositions', 'dispositions/revoke'].includes(String(value.command)) && !positiveId('voucherFileId')) throw new Error('missing correction evidence') if (value.command === 'receipts/correct' && !positiveId('receiptId')) throw new Error('missing original receipt') if (value.command === 'dispositions/revoke' && !positiveId('dispositionId')) throw new Error('missing original disposition') if (value.command === 'dispositions') { const decisions: Record = { SHORTAGE: 'ACCEPT_SHORTAGE', DAMAGED: 'DISPOSE_DAMAGED', EXTRA: 'RETURN_EXTRA' } if (!count('quantity', 1) || !decisions[String(body.kind)] || decisions[String(body.kind)] !== body.decision || (body.kind !== 'SHORTAGE' && !positiveId('receiptId')) || (body.kind === 'SHORTAGE' && body.receiptId != null) || (body.leg === 'DOMESTIC' && !positiveId('sourceCartonId'))) throw new Error('invalid pending disposition') } if (value.command === 'packing/confirm' && !count('packingVersion', 1)) throw new Error('invalid packing version') if (value.command === 'packing') { if (!Array.isArray(body.cartons) || !body.cartons.length) throw new Error('missing packing list') const names = new Set() for (const item of body.cartons) { if (!item || typeof item !== 'object' || typeof item.cartonNo !== 'string' || !item.cartonNo.trim() || names.has(item.cartonNo) || Object.keys(item).some(key => !['cartonNo', 'lengthCm', 'widthCm', 'heightCm', 'grossWeightKg', 'sources'].includes(key))) throw new Error('invalid packing carton') names.add(item.cartonNo) for (const field of ['lengthCm', 'widthCm', 'heightCm', 'grossWeightKg']) { const pattern = field === 'grossWeightKg' ? /^\d{1,9}(?:\.\d{1,3})?$/ : /^\d{1,10}(?:\.\d{1,2})?$/ if (typeof item[field] !== 'string' || !pattern.test(item[field]) || Number(item[field]) <= 0) throw new Error('invalid packing measurement') } if (!Array.isArray(item.sources) || !item.sources.length || item.sources.some((source: unknown) => { if (!source || typeof source !== 'object') return true const row = source as Record return Object.keys(row).some(key => !['receiptId', 'quantity'].includes(key)) || typeof row.receiptId !== 'string' || !/^[1-9]\d*$/.test(row.receiptId) || !Number.isSafeInteger(row.quantity) || Number(row.quantity) < 1 })) throw new Error('invalid packing source') } } if (value.command === 'labels') { const receiver = body.receiver if (!count('packingVersion', 1) || !['OVERSEAS', 'DOMESTIC'].includes(String(body.leg)) || !receiver || typeof receiver !== 'object' || !['name', 'phone', 'address'].every(key => typeof (receiver as Record)[key] === 'string' && String((receiver as Record)[key]).trim())) throw new Error('invalid label address') if (!Array.isArray(body.labels) || !body.labels.length) throw new Error('missing labels') const cartonIds = new Set() for (const row of body.labels) { if (!row || typeof row !== 'object' || typeof row.cartonId !== 'string' || !/^[1-9]\d*$/.test(row.cartonId) || typeof row.fileId !== 'string' || !/^[1-9]\d*$/.test(row.fileId) || !Number.isSafeInteger(row.page) || row.page < 1 || !row.carrier?.trim() || !row.trackingNo?.trim() || cartonIds.has(row.cartonId)) throw new Error('invalid label association') cartonIds.add(row.cartonId) } } if (value.command === 'international-estimate' && (!count('labelVersion', 1) || !/^\d{1,16}(?:\.\d{1,2})?$/.test(String(body.amount)) || !nonempty('businessDate'))) throw new Error('invalid pending estimate') if (['labels/apply', 'overseas-shipment', 'customer-shipment'].includes(String(value.command))) { if (!count('labelVersion', 1) || !Array.isArray(body.cartonIds) || !body.cartonIds.length || body.cartonIds.some(id => typeof id !== 'string' || !/^[1-9]\d*$/.test(id)) || new Set(body.cartonIds).size !== body.cartonIds.length) throw new Error('invalid transport cartons') if (!nonempty(value.command === 'labels/apply' ? 'appliedAt' : 'shippedAt')) throw new Error('missing actual transport time') if ((value.command === 'overseas-shipment' && body.leg !== 'OVERSEAS') || (value.command === 'customer-shipment' && body.leg !== 'DOMESTIC')) throw new Error('wrong shipment leg') } if (value.command === 'domestic-repack') { if (!positiveId('cartonId') || !positiveId('voucherFileId')) throw new Error('invalid repack reference') for (const field of ['lengthCm', 'widthCm', 'heightCm', 'grossWeightKg']) { const pattern = field === 'grossWeightKg' ? /^\d{1,9}(?:\.\d{1,3})?$/ : /^\d{1,10}(?:\.\d{1,2})?$/ if (typeof body[field] !== 'string' || !pattern.test(String(body[field])) || Number(body[field]) <= 0) throw new Error('invalid repack measurement') } } if (String(value.command).startsWith('fees/') && value.command !== 'fees/additional') { if (!positiveId('feeId')) throw new Error('invalid original fee') if (['fees/correct-target', 'fees/refund', 'fees/replace-failed'].includes(String(value.command)) && !positiveId('voucherFileId')) throw new Error('missing finance evidence') if (['fees/correct-target', 'fees/replace-failed'].includes(String(value.command)) && !/^\d{1,16}(?:\.\d{1,2})?$/.test(String(body.targetOriginalAmount))) throw new Error('invalid correction target') if (value.command === 'fees/refund' && (!/^\d{1,16}(?:\.\d{1,2})?$/.test(String(body.amount)) || Number(body.amount) <= 0)) throw new Error('invalid refund amount') } if (value.command === 'charges/confirm') { const selection = (input: unknown) => { if (!input || typeof input !== 'object') throw new Error('missing fee determination') const item = input as Record if ((item.feeId == null) === (item.value == null) || Object.keys(item).some(key => !['feeId', 'value'].includes(key))) throw new Error('ambiguous fee determination') if (item.feeId != null) { if (typeof item.feeId !== 'string' || !/^[1-9]\d*$/.test(item.feeId)) throw new Error('invalid fee reference'); return } if (!item.value || typeof item.value !== 'object') throw new Error('invalid fee amount') const value = item.value as Record if (Object.keys(value).some(key => !['determination', 'amount', 'currency', 'businessDate', 'voucherFileId', 'reason'].includes(key))) throw new Error('unexpected fee amount field') if (!['POSITIVE', 'ZERO', 'NOT_APPLICABLE'].includes(String(value.determination)) || typeof value.amount !== 'string' || !/^\d{1,16}(?:\.\d{1,2})?$/.test(value.amount) || (value.determination === 'POSITIVE' ? Number(value.amount) <= 0 : Number(value.amount) !== 0) || !['CNY', 'USD'].includes(String(value.currency)) || typeof value.businessDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value.businessDate) || typeof value.reason !== 'string' || !value.reason.trim()) throw new Error('invalid fee determination value') if (value.determination === 'POSITIVE' && (typeof value.voucherFileId !== 'string' || !/^[1-9]\d*$/.test(value.voucherFileId))) throw new Error('missing fee voucher') } for (const key of ['internationalFreight', 'duty', 'domesticFreight']) selection(body[key]) if (!['NOT_APPLICABLE', 'ITEMIZED'].includes(String(body.otherDetermination)) || !Array.isArray(body.otherFees) || (body.otherDetermination === 'NOT_APPLICABLE') !== (body.otherFees.length === 0)) throw new Error('invalid other fee determination') const codes = new Set() for (const row of body.otherFees) { if (!row || typeof row !== 'object' || typeof row.code !== 'string' || !/^[A-Z][A-Z0-9_]{0,31}$/.test(row.code) || codes.has(row.code) || typeof row.name !== 'string' || !row.name.trim()) throw new Error('invalid other fee') codes.add(row.code); selection(row.selection) } } if (['control-notes', 'control-notes/resolve', 'actual-warehouse/correct'].includes(String(value.command)) && !positiveId('voucherFileId')) throw new Error('missing control evidence') if (value.command === 'control-notes' && (!['PURCHASE', 'WAREHOUSE', 'RECEIPT', 'SHIPMENT'].includes(String(body.targetType)) || typeof body.blocking !== 'boolean' || (body.targetType === 'PURCHASE' ? body.targetId != null : !positiveId('targetId')))) throw new Error('invalid control target') if (value.command === 'control-notes/resolve' && !positiveId('noteId')) throw new Error('missing original blocker') if (value.command === 'actual-warehouse/correct' && !positiveId('warehouseId')) throw new Error('invalid actual warehouse') if (value.command === 'exception-close') { if (!positiveId('customerVoucherFileId') || !nonempty('financialConclusion') || !nonempty('clientSummary') || !Array.isArray(body.resolutions)) throw new Error('incomplete closure facts') const decisions: Record = { PURCHASE_REMAINDER: ['SUPPLIER_RETAINED', 'RETURNED_TO_SUPPLIER', 'LOSS_CONFIRMED'], TRANSIT_REMAINDER: ['LOSS_CONFIRMED', 'RETURNED_TO_SENDER'], GOOD: ['RETURNED_TO_SUPPLIER', 'CUSTOMER_AUTHORIZED_RETENTION', 'DISPOSED'], DAMAGED: ['DISPOSED', 'RETURNED_TO_SUPPLIER'], EXTRA: ['RETURNED_TO_SUPPLIER'] } const sources = new Set() for (const row of body.resolutions) { if (!row || typeof row !== 'object' || !decisions[row.kind]?.includes(row.decision) || !Number.isSafeInteger(row.quantity) || row.quantity < 1 || typeof row.destination !== 'string' || !row.destination.trim() || typeof row.voucherFileId !== 'string' || !/^[1-9]\d*$/.test(row.voucherFileId) || (row.kind === 'PURCHASE_REMAINDER' ? row.sourceId != null : typeof row.sourceId !== 'string' || !/^[1-9]\d*$/.test(row.sourceId))) throw new Error('invalid closure resolution') const key = `${row.kind}:${row.sourceId ?? ''}`; if (sources.has(key)) throw new Error('duplicate closure source'); sources.add(key) } } if (value.command === 'fees/additional' && (!nonempty('name') || !nonempty('businessDate') || !['CNY', 'USD'].includes(String(body.currency)) || !/^\d{1,16}(?:\.\d{1,2})?$/.test(String(body.amount)) || Number(body.amount) <= 0 || !positiveId('voucherFileId'))) throw new Error('invalid independent financial fee') return value as PendingProcurement } export function definitelyRejected(error: unknown) { const status = error instanceof ApiResponseError ? error.status : isAxiosError(error) ? error.response?.status : undefined const code = error instanceof ApiResponseError ? error.code : '' return [400, 401, 403, 404, 409, 422].includes(status ?? 0) || ['A0400', 'A0401', 'A0403', 'A0404', 'A0409', 'A0422'].includes(code) }