feat: add overseas return operations
This commit is contained in:
@@ -12,6 +12,8 @@ test('all registered user-facing pages avoid rendering internal identifiers', as
|
||||
'../src/pages/operation/operation.vue', '../src/pages/pallet/pallet.vue', '../src/pages/profile/profile.vue',
|
||||
'../src/pages/dispatch/dispatch.vue', '../src/pages/outbound/outbound.vue',
|
||||
'../src/pages/stocktake/stocktake.vue', '../src/pages/move/move.vue', '../src/pages/relabel/relabel.vue', '../src/pages/query/query.vue',
|
||||
'../src/pages/return-receipt/return-receipt.vue',
|
||||
'../src/pages/return-packing/return-packing.vue',
|
||||
'../src/pages/history/history.vue', '../src/pages/scanner-test/scanner-test.vue',
|
||||
'../src/components/PortalHeader.vue', '../src/components/PopupHeader.vue',
|
||||
'../src/components/ReceivingSessionSummary.vue', '../src/components/ScanTargetInput.vue'
|
||||
@@ -40,12 +42,14 @@ test('operator and warehouse cards never fall back to internal IDs', async () =>
|
||||
})
|
||||
|
||||
test('operation templates expose business references only and keep diagnostic evidence', async () => {
|
||||
const [operation, pallet, summary, stocktake, move, relabel, query, apiFiles] = await Promise.all([
|
||||
const [operation, pallet, summary, stocktake, move, relabel, query, returnReceipt, returnPacking, apiFiles] = await Promise.all([
|
||||
read('../src/pages/operation/operation.vue'),
|
||||
read('../src/pages/pallet/pallet.vue'),
|
||||
read('../src/components/ReceivingSessionSummary.vue'),
|
||||
read('../src/pages/stocktake/stocktake.vue'), read('../src/pages/move/move.vue'),
|
||||
read('../src/pages/relabel/relabel.vue'), read('../src/pages/query/query.vue'),
|
||||
read('../src/pages/return-receipt/return-receipt.vue'),
|
||||
read('../src/pages/return-packing/return-packing.vue'),
|
||||
Promise.all([
|
||||
read('../src/api/inbound.js'), read('../src/api/pallet.js'),
|
||||
read('../src/api/overseas.js'), read('../src/api/receiving-adapter.js'),
|
||||
@@ -65,13 +69,15 @@ test('operation templates expose business references only and keep diagnostic ev
|
||||
assert.doesNotMatch(template(pallet), new RegExp(`\\b${field}\\b`))
|
||||
for (const page of [stocktake, move, relabel, query]) assert.doesNotMatch(template(page), new RegExp(`\\b${field}\\b`))
|
||||
}
|
||||
for (const field of [...forbiddenDisplayFields, 'customerId']) assert.doesNotMatch(template(returnReceipt), new RegExp(`\\b${field}\\b`))
|
||||
for (const field of [...forbiddenDisplayFields, 'customerId']) assert.doesNotMatch(template(returnPacking), new RegExp(`\\b${field}\\b`))
|
||||
|
||||
for (const businessField of ['containerNo', 'inboundCode', 'cartonMark', 'palletCode']) {
|
||||
assert.match(operation + pallet + summary, new RegExp(businessField))
|
||||
}
|
||||
assert.match(operation, /requestId[\s\S]*traceId/)
|
||||
assert.match(pallet, /requestId[\s\S]*traceId/)
|
||||
for (const page of [stocktake, move, relabel, query]) assert.match(page, /requestId[\s\S]*traceId/)
|
||||
for (const page of [stocktake, move, relabel, query, returnReceipt, returnPacking]) assert.match(page, /requestId[\s\S]*traceId/)
|
||||
assert.doesNotMatch(apiFiles, /\/api\/platform\/v1\/|inbound-code-relationships/)
|
||||
})
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ test('overseas workbench contains confirmed warehouse and phase-five outbound do
|
||||
const [data, session, runtime] = await Promise.all([
|
||||
read('../src/data/overseas-workbench.js'), read('../src/stores/session.js'), read('../src/config/runtime.js')
|
||||
])
|
||||
for (const label of ['入仓作业', '出库履约', '排车拣货', '出库作业', '库内作业', '换标作业', '现场查询']) assert.match(data, new RegExp(label))
|
||||
for (const label of ['入仓作业', '拆柜收货', '退货入库', '退货合箱', '出库履约', '排车拣货', '出库作业', '库内作业', '换标作业', '现场查询']) assert.match(data, new RegExp(label))
|
||||
for (const excluded of ['集装箱拣货', '装柜取证', '海外派送', '派送单', '客户回访']) assert.doesNotMatch(data, new RegExp(excluded))
|
||||
assert.match(session, /overseas-pda-auth-session/)
|
||||
assert.match(runtime, /VITE_OVERSEAS_API_BASE_URL/)
|
||||
@@ -145,6 +145,42 @@ test('Chinese English and Spanish locale selection is persisted from profile wit
|
||||
assert.match(inbound, /warehouse-profile/)
|
||||
})
|
||||
|
||||
test('return receipt is a real PDA flow backed by the dedicated return APIs', async () => {
|
||||
const [pages, workbench, page, api, warehouseWork] = await Promise.all([
|
||||
read('../src/pages.json'), read('../src/data/overseas-workbench.js'),
|
||||
read('../src/pages/return-receipt/return-receipt.vue'), read('../src/api/return-receipt.js'),
|
||||
read('../src/stores/warehouse-work.js')
|
||||
])
|
||||
assert.match(pages, /pages\/return-receipt\/return-receipt/)
|
||||
assert.match(workbench, /key: 'returnReceipt'[^\n]*available: true[^\n]*route: '\/pages\/return-receipt\/return-receipt'/)
|
||||
for (const action of ['startReturnReceipt', 'findReturnSkuCandidates', 'confirmReturnReceipt']) assert.match(page, new RegExp(action))
|
||||
for (const path of ['/start', '/sku-candidates', '/confirm']) assert.match(api, new RegExp(path.replaceAll('/', '\\/')))
|
||||
assert.match(page, /startScanBroadcast/)
|
||||
assert.match(page, /startedBy: this\.startedBy/)
|
||||
assert.match(page, /idempotencyKey: key\.value/)
|
||||
assert.match(page, /requestId[\s\S]*traceId/)
|
||||
assert.match(warehouseWork, /returnReceipt:[^\n]*pendingKeys/)
|
||||
assert.doesNotMatch(page, /customerId/)
|
||||
})
|
||||
|
||||
test('return packing is a separate same-customer inventory operation', async () => {
|
||||
const [pages, workbench, page, api, warehouseWork] = await Promise.all([
|
||||
read('../src/pages.json'), read('../src/data/overseas-workbench.js'),
|
||||
read('../src/pages/return-packing/return-packing.vue'), read('../src/api/return-packing.js'),
|
||||
read('../src/stores/warehouse-work.js')
|
||||
])
|
||||
assert.match(pages, /pages\/return-packing\/return-packing/)
|
||||
assert.match(workbench, /key: 'returnPacking'[^\n]*available: true[^\n]*route: '\/pages\/return-packing\/return-packing'/)
|
||||
for (const action of ['findPackingOrders', 'mergeReturnCartons']) assert.match(page, new RegExp(action))
|
||||
for (const path of ['/packing/orders', '/cartons/merge', '/cartons/']) assert.match(api, new RegExp(path.replaceAll('/', '\\/')))
|
||||
assert.match(page, /startScanBroadcast/)
|
||||
assert.match(page, /customerKey[\s\S]*不同客户的退货单不能合箱/)
|
||||
assert.match(page, /idempotencyKey: key\.value/)
|
||||
assert.match(page, /requestId[\s\S]*traceId/)
|
||||
assert.match(warehouseWork, /returnPacking:[^\n]*pendingKeys/)
|
||||
assert.doesNotMatch(page.match(/<template>([\s\S]*?)<\/template>/)?.[1] || '', /customerId|warehouseId/)
|
||||
})
|
||||
|
||||
test('home reuses the persisted warehouse profile instead of requesting it on every show', async () => {
|
||||
const [home, inbound, operation] = await Promise.all([read('../src/pages/home/home.vue'), read('../src/api/inbound.js'), read('../src/pages/operation/operation.vue')])
|
||||
assert.match(home, /if \(!this\.user\?\.warehouseCode \|\| !this\.user\?\.warehouseName\) this\.loadWarehouseProfile\(\)/)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { findPackingOrders, getReturnCartonLabel, mergeReturnCartons, normalizePackingPreview } from '../src/api/return-packing.js'
|
||||
import { session } from '../src/stores/session.js'
|
||||
|
||||
test('packing preview keeps customer scope internal and exposes stable order references', () => {
|
||||
const value = normalizePackingPreview({ customerId: '100', orders: [{ returnNo: 'RT-1', customerId: '100', customerReferenceNo: 'REF-1', status: 'COMPLETED' }], skus: [{ skuCode: 'SKU-1', productNameZh: '商品', availableQuantity: 8 }] })
|
||||
assert.equal(value.customerKey, '100')
|
||||
assert.equal(value.orders[0].returnNo, 'RT-1')
|
||||
assert.equal('customerId' in value.orders[0], false)
|
||||
assert.equal(value.skus[0].availableQuantity, 8)
|
||||
})
|
||||
|
||||
test('return packing API uses exact PDA inventory routes without submitting customer or warehouse scope', async () => {
|
||||
const calls = []
|
||||
session.apiBase = '/overseas-api'; session.token = 'packing-token'
|
||||
globalThis.uni = { request(options) { calls.push({ method: options.method, url: options.url, data: options.data, authorization: options.header.Authorization }); let data = {}; if (options.url.includes('/packing/orders')) data = { customerId: '100', orders: [], skus: [] }; else if (options.url.endsWith('/cartons/merge')) data = { cartonMark: 'RC-1', skuCode: 'SKU-1', totalQuantity: 2, label: { cartonMark: 'RC-1', items: [] } }; else data = { cartonMark: 'RC/1', totalQuantity: 2, items: [] }; options.success({ statusCode: 200, data: { code: '200', data }, header: {} }) } }
|
||||
await findPackingOrders('RT/1')
|
||||
const request = { returnNos: ['RT/1'], items: [{ skuCode: 'SKU-1', quantity: 2 }], length: 10, width: 20, height: 30, weight: 4, idempotencyKey: 'merge-key' }
|
||||
await mergeReturnCartons(request)
|
||||
await getReturnCartonLabel('RC/1')
|
||||
assert.deepEqual(calls.map((item) => item.url), [
|
||||
'/overseas-api/pda/v1/last-leg/return-inventory/packing/orders?returnNo=RT%2F1',
|
||||
'/overseas-api/pda/v1/last-leg/return-inventory/cartons/merge',
|
||||
'/overseas-api/pda/v1/last-leg/return-inventory/cartons/RC%2F1/label'
|
||||
])
|
||||
assert.deepEqual(calls[1].data, request)
|
||||
assert.ok(calls.every((item) => item.authorization === 'Bearer packing-token'))
|
||||
assert.ok(calls.every((item) => !Object.hasOwn(item.data || {}, 'customerId') && !Object.hasOwn(item.data || {}, 'warehouseId')))
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { confirmReturnReceipt, findReturnSkuCandidates, normalizeReturnReceiptResult, normalizeReturnReceiptStart, normalizeReturnSkuCandidates, startReturnReceipt } from '../src/api/return-receipt.js'
|
||||
import { session } from '../src/stores/session.js'
|
||||
|
||||
test('return receipt start normalization keeps only stable business facts', () => {
|
||||
assert.deepEqual(normalizeReturnReceiptStart({ returnNo: 'RT-001', status: 'PENDING_RECEIPT', expectedItems: [{ skuCode: 'SKU-1', productNameZh: '商品', expectedQuantity: '5', receivedQuantity: 2 }] }), {
|
||||
returnNo: 'RT-001', status: 'PENDING_RECEIPT', expectedItems: [{ skuCode: 'SKU-1', productNameZh: '商品', expectedQuantity: 5, receivedQuantity: 2 }]
|
||||
})
|
||||
})
|
||||
|
||||
test('SKU candidates do not expose the backend customer identifier to the page', () => {
|
||||
const value = normalizeReturnSkuCandidates({ skuCode: 'SKU-1', candidates: [{ returnNo: 'RT-001', customerId: '100', customerReferenceNo: 'REF-1', status: 'PARTIALLY_RECEIVED', skuExpectedQuantity: 5, skuReceivedQuantity: 1, orderExpectedQuantity: 8, orderReceivedQuantity: 2 }] })
|
||||
assert.equal(value.candidates[0].returnNo, 'RT-001')
|
||||
assert.equal(value.candidates[0].customerReferenceNo, 'REF-1')
|
||||
assert.equal('customerId' in value.candidates[0], false)
|
||||
})
|
||||
|
||||
test('confirmed receipt normalization preserves generated carton and batch references', () => {
|
||||
assert.deepEqual(normalizeReturnReceiptResult({ returnNo: 'RT-001', receiptNo: 'RR-1', receiptBatchNo: 'RB-1', cartonMark: 'RC-1', orderStatus: 'COMPLETED', fullyMatched: true, expectedQuantity: 5, receivedQuantity: 5, signedAt: '2026-09-10T10:00:00' }), {
|
||||
returnNo: 'RT-001', receiptNo: 'RR-1', receiptBatchNo: 'RB-1', cartonMark: 'RC-1', orderStatus: 'COMPLETED', fullyMatched: true, expectedQuantity: 5, receivedQuantity: 5, signedAt: '2026-09-10T10:00:00'
|
||||
})
|
||||
})
|
||||
|
||||
test('return receipt API calls only the dedicated PDA routes and preserves the receipt payload', async () => {
|
||||
const calls = []
|
||||
session.apiBase = '/overseas-api'; session.token = 'return-token'
|
||||
globalThis.uni = {
|
||||
request(options) {
|
||||
calls.push({ method: options.method, url: options.url, data: options.data, authorization: options.header.Authorization })
|
||||
let data
|
||||
if (options.url.endsWith('/start')) data = { returnNo: 'RT-001', status: 'PENDING_RECEIPT', expectedItems: [] }
|
||||
else if (options.url.endsWith('/sku-candidates')) data = { skuCode: 'SKU-1', candidates: [] }
|
||||
else data = { returnNo: 'RT-001', receiptNo: 'RR-1', receiptBatchNo: 'RB-1', cartonMark: 'RC-1', orderStatus: 'PARTIALLY_RECEIVED', fullyMatched: false, expectedQuantity: 5, receivedQuantity: 2 }
|
||||
options.success({ statusCode: 200, data: { code: '200', data }, header: {} })
|
||||
}
|
||||
}
|
||||
await startReturnReceipt(' RT-001 ')
|
||||
await findReturnSkuCandidates(' SKU-1 ')
|
||||
const confirm = { returnNo: 'RT-001', trackingNo: 'TRACK-1', startedBy: 'SKU', cartonMark: 'RC-1', items: [{ skuCode: 'SKU-1', receivedQuantity: 2 }], idempotencyKey: 'return-key' }
|
||||
await confirmReturnReceipt(confirm)
|
||||
assert.deepEqual(calls.map((item) => item.url), [
|
||||
'/overseas-api/pda/v1/last-leg/return-receipts/start',
|
||||
'/overseas-api/pda/v1/last-leg/return-receipts/sku-candidates',
|
||||
'/overseas-api/pda/v1/last-leg/return-receipts/confirm'
|
||||
])
|
||||
assert.deepEqual(calls[0].data, { returnNo: 'RT-001' })
|
||||
assert.deepEqual(calls[1].data, { skuCode: 'SKU-1' })
|
||||
assert.deepEqual(calls[2].data, confirm)
|
||||
assert.ok(calls.every((item) => item.authorization === 'Bearer return-token'))
|
||||
assert.ok(calls.every((item) => !Object.hasOwn(item.data || {}, 'customerId') && !Object.hasOwn(item.data || {}, 'warehouseId')))
|
||||
})
|
||||
@@ -7,6 +7,8 @@ test('only operations backed by a dedicated overseas PDA contract are enabled',
|
||||
const enabled = operations.filter((operation) => operation.available)
|
||||
assert.deepEqual(enabled.map(({ module, name }) => ({ module, name })), [
|
||||
{ module: 'inbound', name: '拆柜收货' },
|
||||
{ module: 'inbound', name: '退货入库' },
|
||||
{ module: 'inbound', name: '退货合箱' },
|
||||
{ module: 'outbound', name: '排车拣货' },
|
||||
{ module: 'outbound', name: '出库作业' },
|
||||
{ module: 'inventory', name: '库存盘点' },
|
||||
@@ -22,6 +24,8 @@ test('only operations backed by a dedicated overseas PDA contract are enabled',
|
||||
assert.equal(operations.find((item) => item.name === '托盘管理')?.route, '/pages/pallet/pallet')
|
||||
assert.equal(operations.find((item) => item.name === '排车拣货')?.route, '/pages/dispatch/dispatch')
|
||||
assert.equal(operations.find((item) => item.name === '出库作业')?.route, '/pages/outbound/outbound')
|
||||
assert.equal(operations.find((item) => item.name === '退货入库')?.route, '/pages/return-receipt/return-receipt')
|
||||
assert.equal(operations.find((item) => item.name === '退货合箱')?.route, '/pages/return-packing/return-packing')
|
||||
|
||||
for (const name of ['库存盘点', '商品移库', '换标收货', '换标执行', '换标复核', '库存查询', '库位查询', '托盘查询']) {
|
||||
const operation = operations.find((item) => item.name === name)
|
||||
|
||||
Reference in New Issue
Block a user