2026-10-10 02:19:39 +08:00
import { describe , expect , it } from 'vitest'
2026-10-10 19:52:33 +08:00
import { buildPacking , buildPhysical , physicalForm , shippedCartons , managementReason , receiptCorrectionReason } from './procurementPhysicalForm'
2026-10-10 02:19:39 +08:00
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' , () => {
2026-10-10 19:52:33 +08:00
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'
2026-10-10 02:19:39 +08: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 ()
})
2026-10-10 19:52:33 +08:00
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 ( '本段已实际交寄' )
})
2026-10-10 02:19:39 +08:00
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' )
})
})