2026-10-04 21:19:11 +08:00
import { beforeEach , describe , expect , it , vi } from 'vitest'
import { http } from '@/shared/api/http'
import { listWarehouseTransferSources } from './warehouseTransferSources'
2026-10-04 22:26:36 +08:00
import { getWarehouseTransfer , type WarehouseTransferDetailDto } from './warehouseTransfers'
import { transferSourceWireFixture } from './warehouseTransferSources.fixture'
2026-10-04 21:19:11 +08:00
vi . mock ( '@/shared/api/http' , async original => ({ ... await original < typeof import ( '@/shared/api/http' ) >(), http : { get : vi . fn () } }))
const transfer : WarehouseTransferDetailDto = { id : 'transfer' , transferNo : 'WT-1' , operatingWarehouseId : 'WH-1' , needRelabel : true , packageCount : 1 , status : 'APPROVED' , items : [], cartons : [], pallets : [], relabelBindings : [{ operationType : 'SKU' , customerCartonMark : 'BOX' , originalSku : 'OLD' , newSku : 'NEW' , quantity : 1 }], sourceOrders : [{ lastlegOrderId : 'tail' , inboundOrderId : 'IN-ID' , inboundCode : 'IN-1' }] }
const balance = { id : 'B-1' , customerId : 'C-1' , overseasWarehouseId : 'WH-1' , inboundOrderId : 'IN-ID' , inboundCode : 'IN-1' , skuCode : 'OLD' , destinationWarehouseCode : 'ABE2' , productNameZh : '货物' }
const position = { id : 'P-1' , balanceId : 'B-1' , customerId : 'C-1' , overseasWarehouseId : 'WH-1' , inboundOrderId : 'IN-ID' , inboundCode : 'IN-1' , palletId : 'PAL-ID' , palletNo : 'DP-1' , warehouseLocationCode : 'LOC-1' , availablePackages : 3 , status : 'OCCUPIED' }
const envelope = ( data : unknown ) => ({ code : '200' , data })
const page = ( records : unknown [], current = 1 , total = records . length ) => envelope ({ records , page : current , size : 200 , total , pages : Math.ceil ( total / 200 ) })
describe ( 'confirmed warehouse transfer source reads' , () => {
beforeEach (() => vi . resetAllMocks ())
2026-10-04 22:26:36 +08:00
it ( 'normalizes the real original-ticket string quantities before source eligibility and package totals' , async () => {
const fixture = transferSourceWireFixture
vi . mocked ( http . get ). mockResolvedValueOnce ( envelope ( fixture . transfer )). mockResolvedValueOnce ( page ([{ id : fixture.balance.id , skuCode : fixture.balance.skuCode }])). mockResolvedValueOnce ( envelope ( fixture . balance )). mockResolvedValueOnce ( envelope ( fixture . positions ))
const detail = await getWarehouseTransfer ( fixture . customerId , fixture . transfer . transferNo )
const result = await listWarehouseTransferSources ( fixture . customerId , detail )
expect ( result . issues ). toEqual ([])
expect ( result . candidates [ 0 ]). toMatchObject ({ availablePackages : 0 , canSelect : false })
expect ( result . candidates [ 1 ]). toMatchObject ({ availablePackages : 3 , canSelect : true })
expect ( detail . packageCount ). toBe ( 1 )
expect ( typeof detail . packageCount ). toBe ( 'number' )
})
2026-10-04 21:19:11 +08:00
it ( 'reads representative detail and actual positions with customer scope, accepts OCCUPIED available stock at original ABE2' , async () => {
vi . mocked ( http . get ). mockResolvedValueOnce ( page ([{ id : 'B-1' , skuCode : 'OLD' }])). mockResolvedValueOnce ( envelope ( balance )). mockResolvedValueOnce ( envelope ([ position , { ... position , id : 'P-HOLD' , availablePackages : 0 , holdPackages : 1 }]))
const result = await listWarehouseTransferSources ( 'C-1' , transfer )
expect ( result . issues ). toEqual ([])
expect ( result . candidates [ 0 ]). toMatchObject ({ palletNo : 'DP-1' , availablePackages : 3 , canSelect : true })
expect ( result . candidates [ 1 ]). toMatchObject ({ canSelect : false , blockReason : '当前无可分配箱数' })
expect ( http . get ). toHaveBeenNthCalledWith ( 1 , '/platform/v1/last-leg/inventory/balances' , { params : { customerId : 'C-1' , overseasWarehouseId : 'WH-1' , inboundCode : 'IN-1' , page : 1 , size : 200 } })
expect ( http . get ). toHaveBeenNthCalledWith ( 2 , '/platform/v1/last-leg/inventory/balances/B-1' , { params : { customerId : 'C-1' } })
expect ( http . get ). toHaveBeenNthCalledWith ( 3 , '/platform/v1/last-leg/inventory/balances/B-1/positions' , { params : { customerId : 'C-1' } })
})
it . each ([ 'customerId' , 'overseasWarehouseId' , 'inboundOrderId' , 'inboundCode' , 'skuCode' ] as const )( 'marks a mismatched representative %s as pending investigation, without treating it as zero inventory' , async key => {
vi . mocked ( http . get ). mockResolvedValueOnce ( page ([{ id : 'B-1' , skuCode : 'OLD' }])). mockResolvedValueOnce ( envelope ({ ... balance , [ key ] : 'WRONG' }))
const result = await listWarehouseTransferSources ( 'C-1' , transfer )
expect ( result . candidates ). toEqual ([]); expect ( result . issues [ 0 ]). toContain ( '待核查' )
expect ( http . get ). toHaveBeenCalledTimes ( 2 )
})
it ( 'does not allow MIXED to stand in for actual old SKU' , async () => {
vi . mocked ( http . get ). mockResolvedValueOnce ( page ([{ id : 'B-1' , skuCode : 'MIXED' }])). mockResolvedValueOnce ( envelope ({ ... balance , skuCode : 'MIXED' })). mockResolvedValueOnce ( envelope ([ position ]))
const result = await listWarehouseTransferSources ( 'C-1' , transfer )
expect ( result . candidates [ 0 ]). toMatchObject ({ skuCode : '—' , canSelect : false })
expect ( result . candidates [ 0 ] ? . blockReason ). toContain ( '具体旧 SKU' )
})
it ( 'checks actual old SKU against the request bindings and rejects missing pallet business identity' , async () => {
vi . mocked ( http . get ). mockResolvedValueOnce ( page ([{ id : 'B-1' , skuCode : 'OTHER' }])). mockResolvedValueOnce ( envelope ({ ... balance , skuCode : 'OTHER' })). mockResolvedValueOnce ( envelope ([ position , { ... position , id : 'P-NO-CODE' , palletNo : null }]))
const result = await listWarehouseTransferSources ( 'C-1' , transfer )
expect ( result . candidates . every ( x => ! x . canSelect )). toBe ( true )
expect ( result . candidates [ 1 ] ? . blockReason ). toContain ( '托盘业务码' )
})
it . each ([ 'customerId' , 'overseasWarehouseId' , 'balanceId' , 'inboundOrderId' , 'inboundCode' ] as const )( 'rejects an actual position with mismatched %s' , async key => {
vi . mocked ( http . get ). mockResolvedValueOnce ( page ([{ id : 'B-1' , skuCode : 'OLD' }])). mockResolvedValueOnce ( envelope ( balance )). mockResolvedValueOnce ( envelope ([{ ... position , [ key ] : 'WRONG' }]))
const result = await listWarehouseTransferSources ( 'C-1' , transfer )
expect ( result . candidates ). toEqual ([]); expect ( result . issues [ 0 ]). toContain ( '待核查' )
})
it ( 'loads the complete second page and rejects changed pagination instead of truncating at 200' , async () => {
vi . mocked ( http . get ). mockImplementation ( async ( url , config ) => {
if ( url === '/platform/v1/last-leg/inventory/balances' ) return page ( Number ( config ? . params . page ) === 1 ? Array . from ({ length : 200 }, ( _ , i ) => ({ id : `B- ${ i } ` , skuCode : 'OLD' })) : [{ id : 'B-200' , skuCode : 'OLD' }], Number ( config ? . params . page ), 201 )
if ( url . endsWith ( '/positions' )) return envelope ([])
return envelope ({ ... balance , id : url.split ( '/' ). at ( - 1 ) })
})
await listWarehouseTransferSources ( 'C-1' , transfer )
expect ( vi . mocked ( http . get ). mock . calls . filter (([ url ]) => url === '/platform/v1/last-leg/inventory/balances' )). toHaveLength ( 2 )
expect ( http . get ). toHaveBeenCalledWith ( '/platform/v1/last-leg/inventory/balances/B-200' , { params : { customerId : 'C-1' } })
vi . mocked ( http . get ). mockReset (). mockResolvedValueOnce ( page ([], 1 , 1 ))
await expect ( listWarehouseTransferSources ( 'C-1' , transfer )). rejects . toThrow ( '分页发生变化或不完整' )
})
it ( 'requires bound sources and old SKU before any inventory call' , async () => {
await expect ( listWarehouseTransferSources ( 'C-1' , { ... transfer , sourceOrders : [] })). rejects . toThrow ( '绑定入仓' )
await expect ( listWarehouseTransferSources ( 'C-1' , { ... transfer , relabelBindings : [] })). rejects . toThrow ( '旧 SKU' )
expect ( http . get ). not . toHaveBeenCalled ()
})
})