fix(lastleg): repair dispatch cancellation and return execution contracts
This commit is contained in:
+55
-6
@@ -9,9 +9,11 @@ import { selectReturnSources, verifyFleetReturnRedispatch, waitForRedispatchSign
|
||||
function fixture(fault = '') {
|
||||
const trip = { tripNo: 'TR-ORIGINAL' }, requests = [], checkpoints = []
|
||||
let returned = false, originalSigned = false, newSigned = false, departed = false
|
||||
const relations = ['A', 'B'].map((name, index) => ({ lastMileOrderId: name, inboundOrderId: String(index + 10), inboundCode: `IN-${name}`,
|
||||
const masterIds = { A: '9007199254740993', B: '9007199254740995' }
|
||||
const relations = ['A', 'B'].map((name, index) => ({ lastMileOrderId: masterIds[name], lastMileOrderNo: `LM-${name}`,
|
||||
customerId: 'C', overseasWarehouseId: 'W', inboundOrderId: fault === 'no-inbound' ? null : String(index + 10), inboundCode: `IN-${name}`,
|
||||
deliveryNo: 'DL-ORIGINAL', outboundNo: 'OUT-ORIGINAL', returnSources: [{ outboundPalletItemId: String(index + 20), inventoryPositionId: String(index + 30), maxReturnPackages: index ? 3 : 4 }] }))
|
||||
const tail = name => ({ orderType: 'FIRST_LEG', sourceInboundId: `DOMESTIC-${name}`, customerId: 'C', overseasWarehouseId: 'W', inboundCode: `IN-${name}`, packageCount: name === 'A' ? 4 : 3,
|
||||
const tail = name => ({ id: masterIds[name], orderNo: `LM-${name}`, deliveryOrderNo: 'DL-ORIGINAL', outboundOrderNo: 'OUT-ORIGINAL', orderType: 'FIRST_LEG', sourceInboundId: `DOMESTIC-${name}`, customerId: 'C', overseasWarehouseId: 'W', inboundCode: `IN-${name}`, packageCount: name === 'A' ? 4 : 3,
|
||||
subOrderStatus: name === 'A' ? newSigned || fault === 'premature-A' && originalSigned ? 'DELIVERY:DELIVERED' : 'DELIVERY:WAIT_DELIVERY'
|
||||
: originalSigned && fault !== 'blocked-B' ? 'DELIVERY:DELIVERED' : 'DELIVERY:WAIT_DELIVERY' })
|
||||
const order = name => name === 'DL-NEW' ? { deliveryNo: name, dispatchNo: 'DP-NEW', packageCount: 2, signedPackages: newSigned ? 2 : null, signedAt: newSigned ? 'new-signature' : null }
|
||||
@@ -28,9 +30,19 @@ function fixture(fault = '') {
|
||||
const delivered = name === 'A' ? newSigned || fault === 'domestic-premature-A' && originalSigned : originalSigned
|
||||
return envelope({ records: [{ inboundCode: code, processStatus: delivered ? 'DELIVERED' : 'LOADED' }] })
|
||||
}
|
||||
if (path.includes('/orders/')) return envelope({ order: tail(path.split('/orders/')[1].split('?')[0]) })
|
||||
if (path.includes('/orders/')) {
|
||||
const masterId = path.split('/orders/')[1].split('?')[0]
|
||||
const name = Object.keys(masterIds).find(name => masterIds[name] === masterId)
|
||||
assert.ok(name, `unknown master ${masterId}`)
|
||||
return envelope({ order: tail(name) })
|
||||
}
|
||||
if (path.includes('/return-inbound-options')) return envelope(fault === 'missing-B' ? relations.slice(0, 1) : relations)
|
||||
if (path.includes('/mark-return')) return envelope({ exceptionNo: 'EX-NEW', version: 0 })
|
||||
if (path.includes('/mark-return')) {
|
||||
assert.equal(body.lastMileOrderId, masterIds.A)
|
||||
assert.equal(Object.hasOwn(body, 'inboundOrderId'), false)
|
||||
assert.deepEqual(body.items, [{ outboundPalletItemId: '20', inventoryPositionId: '30', packageCount: 2 }])
|
||||
return envelope({ exceptionNo: 'EX-NEW', version: 0 })
|
||||
}
|
||||
if (path.includes('/start-processing')) return envelope({ status: 'PROCESSING', version: 1 })
|
||||
if (path.includes('/exception-orders/') && path.includes('/complete')) {
|
||||
const replayed = returned; returned = true
|
||||
@@ -40,7 +52,7 @@ function fixture(fault = '') {
|
||||
if (path.endsWith('/pallets') && method === 'POST') { context.palletCode = body.palletCode; return envelope('100') }
|
||||
if (path.endsWith('/pallets/100')) return envelope({ warehouseId: 'W', locationId: '200', palletCode: context.palletCode })
|
||||
if (path.includes('/locations?')) return envelope({ records: [{ id: '200', locationCode: 'L-1' }] })
|
||||
if (path.includes('/dispatches/candidates')) return envelope([{ sourceType: 'RETURN_RECEIPT', returnReceiptId: '300', returnReceiptNo: 'RSR-300', warehousePalletId: '100', orderId: 'A', packageCount: fault === 'wrong-candidate' ? 4 : 2 }])
|
||||
if (path.includes('/dispatches/candidates')) return envelope([{ sourceType: 'RETURN_RECEIPT', returnReceiptId: '300', returnReceiptNo: 'RSR-300', warehousePalletId: '100', orderId: masterIds.A, packageCount: fault === 'wrong-candidate' ? 4 : 2 }])
|
||||
if (path.endsWith('/drafts/containers')) return envelope({ id: '400' })
|
||||
if (path.endsWith('/draft-locks/lock')) { assert.equal(body.sourceType, 'RETURN_RECEIPT'); assert.equal(body.planPalletId, undefined); return envelope(null) }
|
||||
if (path.endsWith('/pre-arrange')) { assert.deepEqual(body.planPalletIds, []); assert.deepEqual(body.returnReceiptIds, ['300']); return envelope({ dispatchNo: 'DP-NEW', packageCount: 2 }) }
|
||||
@@ -61,7 +73,7 @@ function fixture(fault = '') {
|
||||
async saveReceiptAndComplete(_ctx, delivery) { if (delivery.deliveryNo === 'DL-NEW') newSigned = true; else originalSigned = true; return { order: order(delivery.deliveryNo) } },
|
||||
}
|
||||
const input = { customerId: 'C', warehouseId: 'W', pickerToken: 'private-pda-token', dispatchNos: ['DP-ORIGINAL'], deliveryNos: ['DL-ORIGINAL'] }
|
||||
return { context, trip, input, dependencies, requests, checkpoints }
|
||||
return { context, trip, input, dependencies, requests, checkpoints, relations, masterIds }
|
||||
}
|
||||
const run = f => verifyFleetReturnRedispatch(f.context, 'test', f.trip, f.input, f.dependencies)
|
||||
|
||||
@@ -88,6 +100,43 @@ for (const signedPackages of [undefined, null, '2', 0, 3]) test(`redispatch requ
|
||||
await assert.rejects(run(f), /two boxes actually signed/)
|
||||
})
|
||||
|
||||
test('mark-return uses the selected master ID even without an ordinary inbound relation', async () => {
|
||||
const f = fixture('no-inbound')
|
||||
assert.ok(f.relations.every(row => row.inboundOrderId === null))
|
||||
assert.equal((await run(f)).status, 'HTTP_CHAIN_VERIFIED_SOURCE_EVENT_EVIDENCE_REQUIRED')
|
||||
const writes = f.requests.filter(row => row.path.includes('/mark-return'))
|
||||
assert.equal(writes.length, 1)
|
||||
assert.equal(writes[0].body.lastMileOrderId, f.masterIds.A)
|
||||
assert.equal(Object.hasOwn(writes[0].body, 'inboundOrderId'), false)
|
||||
})
|
||||
|
||||
for (const fault of ['missing-master', 'wrong-delivery', 'wrong-outbound', 'foreign-customer', 'foreign-warehouse', 'borrowed-master']) {
|
||||
test(`return source identity ${fault} stops before mark-return and stock writes`, async () => {
|
||||
const f = fixture(), request = f.context.request.bind(f.context)
|
||||
if (fault === 'missing-master') delete f.relations[0].lastMileOrderId
|
||||
if (fault === 'wrong-delivery') f.relations[0].deliveryNo = 'DL-OTHER'
|
||||
if (fault === 'wrong-outbound') f.relations[0].outboundNo = 'OUT-OTHER'
|
||||
if (fault === 'foreign-customer') f.relations[0].customerId = 'OTHER'
|
||||
if (fault === 'foreign-warehouse') f.relations[0].overseasWarehouseId = 'OTHER'
|
||||
if (fault === 'borrowed-master') f.context.request = async (...args) => {
|
||||
const response = await request(...args)
|
||||
if (args[1].includes(`/orders/${f.masterIds.A}`)) response.body.data.order.id = f.masterIds.B
|
||||
return response
|
||||
}
|
||||
await assert.rejects(run(f), /real master ID|match this candidate/)
|
||||
assert.ok(f.requests.every(row => row.method === 'GET'))
|
||||
assert.equal(f.trip.returnRedispatch.exceptionNo, undefined)
|
||||
})
|
||||
}
|
||||
|
||||
for (const mutate of [source => { source.inventoryPositionId = null }, source => { source.returnCartonId = '701' }]) {
|
||||
test('two-box return never borrows an inventory source from another master or a different source type', async () => {
|
||||
const f = fixture(); mutate(f.relations[0].returnSources[0])
|
||||
await assert.rejects(run(f), /Selected master must contain/)
|
||||
assert.ok(f.requests.every(row => row.method === 'GET'))
|
||||
})
|
||||
}
|
||||
|
||||
test('reviewed candidate recovery retains DELIVERING and never repeats original delivery start', async () => {
|
||||
const f = fixture(); f.input.sourceSelectionRecovery = true
|
||||
assert.equal((await run(f)).status, 'HTTP_CHAIN_VERIFIED_SOURCE_EVENT_EVIDENCE_REQUIRED')
|
||||
|
||||
@@ -113,9 +113,14 @@ export async function verifyFleetReturnRedispatch(context, module, trip, input,
|
||||
}
|
||||
const relations = await request('GET', scoped(`${BASE}/delivery-orders/${encodeURIComponent(number)}/return-inbound-options`, customerId))
|
||||
for (const relation of relations) {
|
||||
check('Return source relation belongs to the original delivery', relation.deliveryNo === number && relation.outboundNo === current.order.outboundNo)
|
||||
check('Return source relation belongs to the original delivery and has a real master ID', /^[1-9][0-9]*$/.test(id(relation.lastMileOrderId))
|
||||
&& id(relation.customerId) === customerId && id(relation.overseasWarehouseId) === warehouseId
|
||||
&& relation.deliveryNo === number && relation.outboundNo === current.order.outboundNo)
|
||||
const detail = await tail(relation)
|
||||
check('Tail source customer and warehouse match this run', id(detail.order.customerId) === customerId && id(detail.order.overseasWarehouseId) === warehouseId)
|
||||
check('Tail source identity, customer and warehouse match this candidate', id(detail.order.id) === id(relation.lastMileOrderId)
|
||||
&& detail.order.orderNo === relation.lastMileOrderNo && detail.order.deliveryOrderNo === number
|
||||
&& detail.order.outboundOrderNo === relation.outboundNo
|
||||
&& id(detail.order.customerId) === customerId && id(detail.order.overseasWarehouseId) === warehouseId)
|
||||
rows.push({ relation, tail: detail.order, delivery: current.order })
|
||||
}
|
||||
}
|
||||
@@ -123,9 +128,12 @@ export async function verifyFleetReturnRedispatch(context, module, trip, input,
|
||||
proof.domesticBefore = { A: await domestic(a), B: await domestic(b) }
|
||||
check('Both source inbounds must be unfinished in this fresh sample', proof.domesticBefore.A.processStatus !== 'DELIVERED' && proof.domesticBefore.B.processStatus !== 'DELIVERED')
|
||||
proof.sources = { A: copy(a), B: copy(b) }; await context.checkpoint()
|
||||
const source = a.relation.returnSources.find(row => Number(row.maxReturnPackages) >= 2)
|
||||
const source = a.relation.returnSources.find(row => /^[1-9][0-9]*$/.test(id(row.outboundPalletItemId))
|
||||
&& /^[1-9][0-9]*$/.test(id(row.inventoryPositionId)) && row.returnCartonId == null
|
||||
&& Number.isSafeInteger(Number(row.maxReturnPackages)) && Number(row.maxReturnPackages) >= 2)
|
||||
check('Selected master must contain the actual ordinary stock source for the two returned boxes', Boolean(source))
|
||||
const marked = await request('POST', scoped(`${BASE}/delivery-orders/${a.delivery.deliveryNo}/mark-return`, customerId), {
|
||||
inboundOrderId: a.relation.inboundOrderId, reason: `${context.marker} two boxes refused before signature`, returnDirection: 'OVERSEAS_WAREHOUSE',
|
||||
lastMileOrderId: id(a.relation.lastMileOrderId), reason: `${context.marker} two boxes refused before signature`, returnDirection: 'OVERSEAS_WAREHOUSE',
|
||||
returnLogisticsNo: `${context.marker}-RET`, items: [{ outboundPalletItemId: source.outboundPalletItemId, inventoryPositionId: source.inventoryPositionId, packageCount: 2 }], idempotencyKey: key(),
|
||||
})
|
||||
proof.exceptionNo = marked.exceptionNo
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test'
|
||||
import { relabelOutboundMarksMatch, inventoryQuantitySnapshot, inventorySnapshot, dispatchInventorySources, inventorySourcesPresent, dispatchStockConservation, verifyDepartureReadiness, verifyPartialOrderCompletion, verifyFulfillmentRootStatus,
|
||||
completeReturnedException, returnedExceptionFactsMatch, verifyCustomerSignedDocuments, createSignedDocumentIsolationCustomer,
|
||||
createDeliveryReturn, completeReturnedException, returnedExceptionFactsMatch, verifyCustomerSignedDocuments, createSignedDocumentIsolationCustomer,
|
||||
dispatchDestinationConservation } from './phase5-overseas-fulfillment.mjs'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
@@ -105,7 +105,7 @@ test('phase-five module proves receipt-save completion, cross-region convergence
|
||||
"visitResult: 'SATISFIED'", '/exception-orders',
|
||||
'/return-inbound-options', '/mark-return', "returnDirection: 'OVERSEAS_WAREHOUSE'",
|
||||
'returnSources', 'outboundPalletItemId', 'inventoryPositionId',
|
||||
'value.returnSources.some', 'returnInbound?.deliveryNo === deliveryNo',
|
||||
'value.returnSources.some', 'returnCandidate?.deliveryNo === deliveryNo',
|
||||
"idempotencyKey: key(context, 'DELIVERY-RETURN')", '派送退回同键同载荷重放',
|
||||
'RETURN_PENDING_WITH_COMPLETED_VISIT',
|
||||
]) assert.ok(source.includes(token), token)
|
||||
@@ -183,6 +183,68 @@ function contextFor(request) {
|
||||
}
|
||||
const ok = data => ({ http: 200, body: { code: '200', data } })
|
||||
|
||||
function deliveryReturnFixture() {
|
||||
return { lastMileOrderId: '9007199254740993', lastMileOrderNo: 'LM-1', inboundOrderId: '81', inboundCode: 'IN-A',
|
||||
customerId: '100', overseasWarehouseId: 'WH-1', deliveryNo: 'DL-1', outboundNo: 'OB-1',
|
||||
returnSources: [{ outboundPalletItemId: '51', inventoryPositionId: '61', returnCartonId: null, maxReturnPackages: 2 }] }
|
||||
}
|
||||
const returnScope = { customerId: '100', warehouseId: 'WH-1', deliveryNo: 'DL-1', outboundNo: 'OB-1' }
|
||||
|
||||
for (const ordinaryInbound of ['81', null, undefined]) test(`phase5 mark-return sends the actual master key; inbound=${ordinaryInbound}`, async () => {
|
||||
const candidate = deliveryReturnFixture(); candidate.inboundOrderId = ordinaryInbound
|
||||
if (!ordinaryInbound) candidate.inboundCode = null
|
||||
const context = contextFor((method, path, body) => {
|
||||
if (method === 'GET') { assert.match(path, /return-inbound-options/); return ok([candidate]) }
|
||||
assert.equal(method, 'POST'); assert.match(path, /mark-return/)
|
||||
assert.equal(body.lastMileOrderId, candidate.lastMileOrderId)
|
||||
assert.equal(Object.hasOwn(body, 'inboundOrderId'), false)
|
||||
assert.deepEqual(body.items, [{ outboundPalletItemId: '51', inventoryPositionId: '61', packageCount: 1 }])
|
||||
assert.equal(body.returnDirection, 'OVERSEAS_WAREHOUSE')
|
||||
assert.ok(body.idempotencyKey)
|
||||
return ok({ exceptionNo: 'EX-1', status: 'PENDING', version: 0 })
|
||||
})
|
||||
const result = await createDeliveryReturn(context, returnScope)
|
||||
assert.equal(result.returned.data.exceptionNo, 'EX-1')
|
||||
assert.equal(context.calls.filter(row => row.method === 'POST').length, 1)
|
||||
assert.deepEqual(context.assertions.filter(row => !row.passed), [])
|
||||
})
|
||||
|
||||
test('phase5 keeps the selected master and its own source together when candidates share no inbound relation', async () => {
|
||||
const first = deliveryReturnFixture(); first.inboundOrderId = null; first.inboundCode = null; first.returnSources = []
|
||||
const selected = { ...deliveryReturnFixture(), lastMileOrderId: '9007199254740995', lastMileOrderNo: 'LM-2', inboundOrderId: null, inboundCode: null,
|
||||
returnSources: [{ outboundPalletItemId: '52', inventoryPositionId: '62', maxReturnPackages: 1 }] }
|
||||
const context = contextFor((method, path, body) => {
|
||||
if (method === 'GET') return ok([first, selected])
|
||||
assert.equal(body.lastMileOrderId, selected.lastMileOrderId)
|
||||
assert.equal(Object.hasOwn(body, 'inboundOrderId'), false)
|
||||
assert.deepEqual(body.items, [{ outboundPalletItemId: '52', inventoryPositionId: '62', packageCount: 1 }])
|
||||
return ok({ exceptionNo: 'EX-2' })
|
||||
})
|
||||
await createDeliveryReturn(context, returnScope)
|
||||
assert.equal(context.calls.filter(row => row.method === 'POST').length, 1)
|
||||
})
|
||||
|
||||
for (const [name, mutate] of [
|
||||
['missing-master', row => { delete row.lastMileOrderId }], ['zero-master', row => { row.lastMileOrderId = '0' }],
|
||||
['foreign-customer', row => { row.customerId = 'OTHER' }], ['foreign-warehouse', row => { row.overseasWarehouseId = 'OTHER' }],
|
||||
['wrong-delivery', row => { row.deliveryNo = 'DL-OTHER' }], ['wrong-outbound', row => { row.outboundNo = 'OB-OTHER' }],
|
||||
['missing-source', row => { row.returnSources = [] }], ['missing-position', row => { row.returnSources[0].inventoryPositionId = null }],
|
||||
['ambiguous-source-type', row => { row.returnSources[0].returnCartonId = '701' }],
|
||||
]) test(`phase5 refuses ${name} before any mark-return write even with an inbound ID`, async () => {
|
||||
const candidate = deliveryReturnFixture(); mutate(candidate)
|
||||
const context = contextFor((method) => { assert.equal(method, 'GET'); return ok([candidate]) })
|
||||
assert.equal(await createDeliveryReturn(context, returnScope), null)
|
||||
assert.equal(context.calls.length, 1)
|
||||
assert.ok(context.assertions.some(row => !row.passed))
|
||||
})
|
||||
|
||||
test('the phase5 lifecycle calls the tested request path and replays its exact returned body', () => {
|
||||
assert.match(source, /const returnRequest = await createDeliveryReturn\(context, \{ customerId, warehouseId, deliveryNo, outboundNo \}\)/)
|
||||
assert.match(source, /const \{ returned, returnBody \} = returnRequest/)
|
||||
assert.doesNotMatch(source, /inboundOrderId:\s*id\(returnCandidate/)
|
||||
assert.match(source, /lastMileOrderId: id\(returnCandidate\.lastMileOrderId\)/)
|
||||
})
|
||||
|
||||
test('partial completion rejects different Orders and an already delivered pending fragment', async () => {
|
||||
for (const second of [{ orderId: 'OTHER', processStatus: 'WAIT_SHIP' },
|
||||
{ orderId: 'ORDER', processStatus: 'DELIVERED' }]) {
|
||||
|
||||
@@ -892,6 +892,42 @@ export async function pickAll(context, token, dispatchNo) {
|
||||
return success(replay.response)
|
||||
}
|
||||
|
||||
/** 只从本次派送的真实主单及其原出库来源创建退回请求,不要求普通入仓关联。 */
|
||||
export async function createDeliveryReturn(context, { customerId, warehouseId, deliveryNo, outboundNo }) {
|
||||
const eligibleSource = source => /^[1-9][0-9]*$/.test(id(source?.outboundPalletItemId))
|
||||
&& /^[1-9][0-9]*$/.test(id(source?.inventoryPositionId)) && source?.returnCartonId == null
|
||||
&& Number.isSafeInteger(Number(source?.maxReturnPackages)) && Number(source.maxReturnPackages) > 0
|
||||
const returnOptions = await platform(context, 'GET', '查询派送退回可选主单',
|
||||
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/return-inbound-options`, { customerId }))
|
||||
const returnCandidate = Array.isArray(returnOptions.data)
|
||||
? returnOptions.data.find((value) => /^[1-9][0-9]*$/.test(id(value?.lastMileOrderId))
|
||||
&& id(value?.customerId) === customerId && id(value?.overseasWarehouseId) === warehouseId
|
||||
&& value?.deliveryNo === deliveryNo && value?.outboundNo === outboundNo
|
||||
&& Array.isArray(value?.returnSources) && value.returnSources.some(eligibleSource)) : null
|
||||
context.assert(MODULE, '退回候选严格属于当前派送单', returnOptions.response,
|
||||
id(returnCandidate?.lastMileOrderId) && returnCandidate?.deliveryNo === deliveryNo && returnCandidate?.outboundNo === outboundNo,
|
||||
`candidate=${Boolean(returnCandidate)}; ${trace(returnOptions.response)}`, OPTIONS)
|
||||
if (!returnCandidate) return null
|
||||
const returnSource = Array.isArray(returnCandidate.returnSources)
|
||||
? returnCandidate.returnSources.find(eligibleSource) : null
|
||||
context.assert(MODULE, '退回候选包含可追溯的出库托盘库存来源', returnOptions.response,
|
||||
Boolean(returnSource), `source=${Boolean(returnSource)}; ${trace(returnOptions.response)}`, OPTIONS)
|
||||
if (!returnSource) return null
|
||||
const returnBody = {
|
||||
lastMileOrderId: id(returnCandidate.lastMileOrderId), reason: `${context.marker}-delivery-return`,
|
||||
returnDirection: 'OVERSEAS_WAREHOUSE', returnLogisticsNo: `${context.marker}-RETURN`.slice(0, 128),
|
||||
items: [{
|
||||
outboundPalletItemId: id(returnSource.outboundPalletItemId),
|
||||
inventoryPositionId: id(returnSource.inventoryPositionId),
|
||||
packageCount: 1,
|
||||
}],
|
||||
idempotencyKey: key(context, 'DELIVERY-RETURN'),
|
||||
}
|
||||
const returned = await platform(context, 'POST', '标记派送退回海外仓并创建退货异常',
|
||||
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/mark-return`, { customerId }), returnBody)
|
||||
return { returned, returnBody }
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
context.createdResources.phase5OverseasFulfillment = {
|
||||
marker: context.marker, dispatchNo: null, outboundNo: null, deliveryNo: null, deliveryNos: [],
|
||||
@@ -1196,34 +1232,9 @@ export async function run(context) {
|
||||
nonBlank(manualExceptionNo) && manualException.data?.status === 'PENDING', trace(manualException.response), OPTIONS)
|
||||
context.createdResources.phase5OverseasFulfillment.manualExceptionNo = manualExceptionNo || null
|
||||
|
||||
const returnOptions = await platform(context, 'GET', '查询派送退回可选入仓单',
|
||||
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/return-inbound-options`, { customerId }))
|
||||
const returnInbound = Array.isArray(returnOptions.data)
|
||||
? returnOptions.data.find((value) => Array.isArray(value?.returnSources)
|
||||
&& value.returnSources.some((source) => id(source?.outboundPalletItemId)
|
||||
&& id(source?.inventoryPositionId) && Number(source?.maxReturnPackages) > 0)) : null
|
||||
context.assert(MODULE, '退回候选严格属于当前派送单', returnOptions.response,
|
||||
id(returnInbound?.inboundOrderId) && returnInbound?.deliveryNo === deliveryNo && returnInbound?.outboundNo === outboundNo,
|
||||
`candidate=${Boolean(returnInbound)}; ${trace(returnOptions.response)}`, OPTIONS)
|
||||
if (!returnInbound) return
|
||||
const returnSource = Array.isArray(returnInbound.returnSources)
|
||||
? returnInbound.returnSources.find((value) => id(value?.outboundPalletItemId)
|
||||
&& id(value?.inventoryPositionId) && Number(value?.maxReturnPackages) > 0) : null
|
||||
context.assert(MODULE, '退回候选包含可追溯的出库托盘库存来源', returnOptions.response,
|
||||
Boolean(returnSource), `source=${Boolean(returnSource)}; ${trace(returnOptions.response)}`, OPTIONS)
|
||||
if (!returnSource) return
|
||||
const returnBody = {
|
||||
inboundOrderId: id(returnInbound.inboundOrderId), reason: `${context.marker}-delivery-return`,
|
||||
returnDirection: 'OVERSEAS_WAREHOUSE', returnLogisticsNo: `${context.marker}-RETURN`.slice(0, 128),
|
||||
items: [{
|
||||
outboundPalletItemId: id(returnSource.outboundPalletItemId),
|
||||
inventoryPositionId: id(returnSource.inventoryPositionId),
|
||||
packageCount: 1,
|
||||
}],
|
||||
idempotencyKey: key(context, 'DELIVERY-RETURN'),
|
||||
}
|
||||
const returned = await platform(context, 'POST', '标记派送退回海外仓并创建退货异常',
|
||||
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/mark-return`, { customerId }), returnBody)
|
||||
const returnRequest = await createDeliveryReturn(context, { customerId, warehouseId, deliveryNo, outboundNo })
|
||||
if (!returnRequest) return
|
||||
const { returned, returnBody } = returnRequest
|
||||
const returnExceptionNo = id(returned.data?.exceptionNo)
|
||||
context.assert(MODULE, '派送退回生成独立PENDING异常单', returned.response,
|
||||
nonBlank(returnExceptionNo) && returned.data?.status === 'PENDING'
|
||||
|
||||
Reference in New Issue
Block a user