Files
admin-web/src/modules/overseas-warehouse/api/warehouseTransferSources.test.ts
T

77 lines
7.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/shared/api/http'
import { listWarehouseTransferSources } from './warehouseTransferSources'
import { getWarehouseTransfer, type WarehouseTransferDetailDto } from './warehouseTransfers'
import { transferSourceWireFixture } from './warehouseTransferSources.fixture'
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())
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')
})
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()
})
})