122 lines
12 KiB
TypeScript
122 lines
12 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { buildPacking, buildPhysical, physicalForm, shippedCartons, managementReason, receiptCorrectionReason } from './procurementPhysicalForm'
|
|
import { readPendingProcurement } from './procurementPending'
|
|
import type { PlatformProcurement } from './procurementTypes'
|
|
const base = { version: 7, idempotencyKey: 'physical-one', reason: '现场核对' }
|
|
function detail(): PlatformProcurement {
|
|
return {
|
|
customerView: { order: { id: '1', orderNo: 'OP-1', customerId: '10001', customerName: '客户', purchaseCategory: '样品', productName: '外套', productSpecification: null, productUrl: null, purchaseQuantity: 10, budgetUnitPrice: '1.00', currency: 'CNY', receivingAddress: '深圳', contactName: '收件人', contactPhone: '13800000000', estimatedArrivalAt: null, plannedWarehouseName: '海外仓', actualWarehouseName: '海外仓', actualWarehouseCode: 'LAX', status: 'OVERSEAS_RECEIVING', version: 7, createdAt: '', updatedAt: '' }, budget: { totalAmount: '10.00', currency: 'CNY', authorizedAt: null }, purchased: true, purchaseConfirmed: true, purchaseNeedsAttention: false, progress: [], boxes: [], shipments: [{ shipmentNo: 'PS-1', leg: 'OVERSEAS', cartonNo: 'C1', quantity: 5, shippedAt: '2020-01-03T10:00:00', carrier: 'UPS', trackingNo: 'TRACK-1' }], fees: [], exceptionSummaries: [] },
|
|
purchaseFacts: { actual_warehouse_id: '20', purchased_quantity: 10, purchased_at: '2020-01-01T10:00:00', overseas_shipped_at: '2020-01-03T10:00:00' },
|
|
physical: { receipts: [{ id: '11', receiptNo: 'PR-11', leg: 'OVERSEAS', sourceCartonId: null, goodQuantity: 6, damagedQuantity: 1, extraQuantity: 2, receivedAt: '2020-01-02T10:00:00', active: true }, { id: '12', receiptNo: 'PR-12', leg: 'OVERSEAS', sourceCartonId: null, goodQuantity: 3, damagedQuantity: 0, extraQuantity: 0, receivedAt: '2020-01-02T12:00:00', active: true }], dispositions: [], packingVersion: 2, packingStatus: 'DRAFT', cartons: [{ id: '31', cartonNo: 'C1', quantity: 5, lengthCm: '10.00', widthCm: '20.00', heightCm: '30.00', grossWeightKg: '1.234', sources: [{ receiptId: '11', quantity: 5 }] }, { id: '32', cartonNo: 'C2', quantity: 4, lengthCm: '10.00', widthCm: '20.00', heightCm: '30.00', grossWeightKg: '1.234', sources: [{ receiptId: '11', quantity: 1 }, { receiptId: '12', quantity: 3 }] }] },
|
|
transport: { overseasCargo: [], domesticCargo: [], overseasLabels: null, domesticLabels: null, shipments: [] }, fees: [], feeCorrections: [], controls: { notes: [], remaining: [], closures: [], closureItems: [] }, files: [], reviewHistory: [], operations: [], actions: [], internalRemark: null,
|
|
}
|
|
}
|
|
describe('procurement physical request validation', () => {
|
|
it('builds multi-carton allocations with exact dimensions, retaining independent source references', () => {
|
|
const value = detail(); const form = physicalForm(value)
|
|
const payload = buildPhysical('packing', form, value, base, '', '')
|
|
expect(payload.cartons?.[1].sources).toEqual([{ receiptId: '11', quantity: 1 }, { receiptId: '12', quantity: 3 }])
|
|
expect(payload.cartons?.[0].grossWeightKg).toBe('1.234')
|
|
expect(readPendingProcurement(JSON.stringify({ command: 'packing', payload }))).toEqual({ command: 'packing', payload })
|
|
form.cartons[0].sources[0].quantity = 1
|
|
expect(payload.cartons?.[0].sources[0].quantity).toBe(5)
|
|
})
|
|
it('rejects repeated boxes, duplicate same-box sources, missing sources and cross-box over-allocation', () => {
|
|
const value = detail(); let boxes = physicalForm(value).cartons
|
|
boxes[1].cartonNo = 'C1'; expect(() => buildPacking(boxes, value)).toThrow('箱号')
|
|
boxes = physicalForm(value).cartons; boxes[0].sources.push({ receiptId: '11', quantity: 1 }); expect(() => buildPacking(boxes, value)).toThrow('同箱来源不可重复')
|
|
boxes = physicalForm(value).cartons; boxes[0].sources = []; expect(() => buildPacking(boxes, value)).toThrow('每箱必须')
|
|
boxes = physicalForm(value).cartons; boxes[0].sources[0].quantity = 6; expect(() => buildPacking(boxes, value)).toThrow('超过实收 PR-11')
|
|
})
|
|
it('rejects fractional pieces, inactive receipts and excessive measurement precision', () => {
|
|
const value = detail(); let boxes = physicalForm(value).cartons
|
|
boxes[0].sources[0].quantity = 1.5; expect(() => buildPacking(boxes, value)).toThrow('整数')
|
|
boxes = physicalForm(value).cartons; boxes[0].lengthCm = '10.001'; expect(() => buildPacking(boxes, value)).toThrow('最多 2 位')
|
|
boxes = physicalForm(value).cartons; boxes[0].grossWeightKg = '1.0001'; expect(() => buildPacking(boxes, value)).toThrow('最多 3 位')
|
|
value.physical.receipts[0].active = false; expect(() => buildPacking(physicalForm(value).cartons, value)).toThrow('有效海外实收')
|
|
})
|
|
it('requires domestic receipts to use an actually shipped original carton and excludes unshipped boxes', () => {
|
|
const value = detail(); const form = physicalForm(); form.leg = 'DOMESTIC'; form.receivedAt = '2020-01-04T10:00:00'; form.goodQuantity = 3
|
|
expect(shippedCartons(value).map(row => row.cartonNo)).toEqual(['C1'])
|
|
form.sourceCartonId = '32'; expect(() => buildPhysical('domestic-receipts', form, value, base, '', '')).toThrow('已国际交寄')
|
|
form.sourceCartonId = '31'; const payload = buildPhysical('domestic-receipts', form, value, base, '', '')
|
|
expect(payload).toMatchObject({ leg: 'DOMESTIC', sourceCartonId: '31', goodQuantity: 3, damagedQuantity: 0, extraQuantity: 0 }); expect(payload).not.toHaveProperty('warehouseId')
|
|
expect(readPendingProcurement(JSON.stringify({ command: 'domestic-receipts', payload }))).not.toBeNull()
|
|
value.physical.receipts.push({ id: '13', receiptNo: 'PR-13', leg: 'DOMESTIC', sourceCartonId: '31', goodQuantity: 4, damagedQuantity: 0, extraQuantity: 0, receivedAt: '2020-01-04T10:00:00', active: true })
|
|
expect(() => buildPhysical('domestic-receipts', form, value, base, '', '')).toThrow('超过来源件数')
|
|
})
|
|
it('preserves the fixed overseas warehouse, rejects future receipts and keeps extras separate', () => {
|
|
const value = detail(); value.physical.receipts = []; const form = physicalForm(); form.goodQuantity = 8; form.damagedQuantity = 2; form.extraQuantity = 3; form.receivedAt = '2020-01-02T10:00:00'
|
|
expect(() => buildPhysical('overseas-receipts', form, value, base, 'other', '')).toThrow('已冻结')
|
|
expect(buildPhysical('overseas-receipts', form, value, base, '20', '')).toMatchObject({ goodQuantity: 8, damagedQuantity: 2, extraQuantity: 3, warehouseId: '20' })
|
|
form.receivedAt = '2999-01-01T10:00:00'; expect(() => buildPhysical('overseas-receipts', form, value, base, '20', '')).toThrow('真实实收时间')
|
|
})
|
|
it('binds a damaged disposition to its receipt and prevents repeat disposal', () => {
|
|
const value = detail(); const form = physicalForm(); form.kind = 'DAMAGED'; form.receiptId = '11'; form.quantity = 1
|
|
const payload = buildPhysical('dispositions', form, value, base, '', '901')
|
|
expect(payload).toMatchObject({ kind: 'DAMAGED', decision: 'DISPOSE_DAMAGED', receiptId: '11', quantity: 1, voucherFileId: '901' })
|
|
expect(readPendingProcurement(JSON.stringify({ command: 'dispositions', payload }))).not.toBeNull()
|
|
value.physical.dispositions.push({ id: '71', leg: 'OVERSEAS', source_carton_id: null, receipt_id: '11', kind: 'DAMAGED', decision: 'DISPOSE_DAMAGED', quantity: 1, voucher_file_id: '901', reason: '已处置', active: true, created_at: '' })
|
|
expect(() => buildPhysical('dispositions', form, value, base, '', '901')).toThrow('未处理差异')
|
|
})
|
|
it('binds shortage to the purchase instead of fabricating a receipt', () => {
|
|
const value = detail(); value.physical.receipts.pop(); const form = physicalForm(); form.kind = 'SHORTAGE'; form.quantity = 3; form.receiptId = 'stale'
|
|
const payload = buildPhysical('dispositions', form, value, base, '', '901')
|
|
expect(payload).toMatchObject({ kind: 'SHORTAGE', decision: 'ACCEPT_SHORTAGE', quantity: 3 }); expect(payload).not.toHaveProperty('receiptId')
|
|
})
|
|
it('corrects an original receipt without counting it twice or moving its origin', () => {
|
|
const value = detail(); value.purchaseFacts.overseas_shipped_at = null; const form = physicalForm(); form.receiptId = '11'; form.leg = 'DOMESTIC'; form.sourceCartonId = '31'; form.goodQuantity = 5; form.damagedQuantity = 1; form.receivedAt = '2020-01-02T10:00:00'
|
|
const payload = buildPhysical('receipts/correct', form, value, base, '20', '901')
|
|
expect(payload).toMatchObject({ receiptId: '11', leg: 'OVERSEAS', goodQuantity: 5 }); expect(payload).not.toHaveProperty('sourceCartonId')
|
|
expect(readPendingProcurement(JSON.stringify({ command: 'receipts/correct', payload }))).not.toBeNull()
|
|
})
|
|
it('blocks shipped receipt corrections while preserving domestic corrections before customer shipment', () => {
|
|
const value = detail()
|
|
expect(receiptCorrectionReason(value, 'OVERSEAS')).toContain('本段已实际交寄')
|
|
expect(receiptCorrectionReason(value, 'DOMESTIC')).toBeNull()
|
|
value.purchaseFacts.customer_shipped_at = '2020-01-05T10:00:00'
|
|
expect(receiptCorrectionReason(value, 'DOMESTIC')).toContain('本段已实际交寄')
|
|
value.purchaseFacts.overseas_shipped_at = null
|
|
value.physical.packingStatus = 'CONFIRMED'
|
|
expect(receiptCorrectionReason(value, 'OVERSEAS')).toContain('先撤回')
|
|
value.physical.packingStatus = 'DRAFT'
|
|
expect(receiptCorrectionReason(value, 'OVERSEAS')).toBeNull()
|
|
})
|
|
it.each(['REJECTED', 'CANCELLED', 'EXCEPTION_CLOSED', 'SHIPPED_TO_CUSTOMER'])('blocks physical changes for %s but leaves evidence and warehouse correction available', status => {
|
|
const value = detail(); value.customerView.order.status = status
|
|
expect(managementReason('receipts/correct', value)).toContain('订单已结束')
|
|
expect(managementReason('dispositions', value)).toContain('订单已结束')
|
|
expect(managementReason('packing/withdraw', value)).toContain('订单已结束')
|
|
expect(managementReason('control-notes', value)).toBeNull()
|
|
expect(managementReason('actual-warehouse/correct', value)).toBeNull()
|
|
const form = physicalForm(); form.receiptId = '11'
|
|
expect(() => buildPhysical('receipts/correct', form, value, base, '20', '901')).toThrow('订单已结束')
|
|
})
|
|
it('requires the correct physical source and unfinished receipt before recording differences', () => {
|
|
const value = detail(); value.purchaseFacts = {}
|
|
expect(managementReason('dispositions', value)).toContain('交寄来源')
|
|
value.purchaseFacts.purchased_at = '2020-01-01'
|
|
expect(managementReason('dispositions', value)).toBeNull()
|
|
value.purchaseFacts.overseas_received_at = '2020-01-02'
|
|
expect(managementReason('dispositions', value)).not.toBeNull()
|
|
value.purchaseFacts.overseas_shipped_at = '2020-01-03'
|
|
expect(managementReason('dispositions', value)).toBeNull()
|
|
value.purchaseFacts.domestic_received_at = '2020-01-04'
|
|
expect(managementReason('dispositions', value)).toContain('本段收货已完成')
|
|
})
|
|
it('rejects a shipped receipt even when another leg still offers a correctable receipt', () => {
|
|
const value = detail()
|
|
value.physical.receipts.push({ ...value.physical.receipts[0]!, id: '13', leg: 'DOMESTIC', sourceCartonId: '31' })
|
|
expect(managementReason('receipts/correct', value)).toBeNull()
|
|
const form = physicalForm(); form.receiptId = '11'
|
|
expect(() => buildPhysical('receipts/correct', form, value, base, '20', '901')).toThrow('本段已实际交寄')
|
|
})
|
|
it('confirms the exact draft version and rejects malformed persisted packing before replay', () => {
|
|
const value = detail(); const form = physicalForm(value)
|
|
expect(buildPhysical('packing/confirm', form, value, base, '', '')).toEqual({ ...base, packingVersion: 2 })
|
|
value.physical.packingStatus = 'CONFIRMED'; expect(() => buildPhysical('packing/confirm', form, value, base, '', '')).toThrow('装箱草稿')
|
|
const payload = { ...base, cartons: [{ cartonNo: 'C1', lengthCm: '1', widthCm: '1', heightCm: '1', grossWeightKg: '1', sources: [{ receiptId: '11', quantity: 0 }] }] }
|
|
expect(() => readPendingProcurement(JSON.stringify({ command: 'packing', payload }))).toThrow('packing source')
|
|
})
|
|
})
|