fix(acceptance): close overseas dropship pallet chain
This commit is contained in:
+1
-1
@@ -344,7 +344,7 @@ public class ReceivingContainerService {
|
||||
if (!"OVERSEAS".equals(destinationType(ref.line(), ref.order()))) {
|
||||
throw conflict("FBA destination must use bulk count");
|
||||
}
|
||||
WarehouseResourceGateway.WarehousePalletReference resource = warehouse.resolvePallet(request.palletCode());
|
||||
WarehouseResourceGateway.WarehousePalletReference resource = warehouse.inspectPallet(request.palletCode());
|
||||
validatePallet(ref, resource);
|
||||
InboundPlanPallet pallet = choosePallet(ref, resource);
|
||||
workflow.validateCartonResolved(customerId, ref.plan(), ref.line(), pallet, request.cartonMark());
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ public class PdaInventoryMoveService {
|
||||
PdaInventoryMove existing = moves.find(warehouseId, operatorKey, key);
|
||||
if (existing != null) return replay(existing, requestHash);
|
||||
|
||||
var pallet = warehouse.resolvePallet(required(request.sourcePalletCode(), "Source pallet code is required"));
|
||||
var pallet = warehouse.inspectPallet(required(request.sourcePalletCode(), "Source pallet code is required"));
|
||||
if (pallet == null) throw notFound("Source pallet was not found");
|
||||
if (pallet.warehouseId() != warehouseId) throw conflict("Source pallet belongs to another overseas warehouse");
|
||||
var target = warehouse.resolveLocation(warehouseId,
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ public class PdaStocktakeService {
|
||||
long resourceId;
|
||||
List<StocktakeCandidate> candidates;
|
||||
if ("PALLET".equals(scopeType)) {
|
||||
var pallet = warehouse.resolvePallet(scopeCode);
|
||||
var pallet = warehouse.inspectPallet(scopeCode);
|
||||
if (pallet.warehouseId() != warehouseId) throw conflict("Pallet belongs to another overseas warehouse");
|
||||
resourceId = pallet.palletId();
|
||||
scopeCode = pallet.palletNo();
|
||||
|
||||
+1
-1
@@ -399,7 +399,7 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
org.mockito.Mockito.reset(plans, lines, orders, sessions, operations, cartons, warehouse, pallets, workflow);
|
||||
stubCurrent("OVERSEAS", "CONT-A");
|
||||
when(cartons.selectOne(any())).thenReturn(carton(1L, "BOX-1"));
|
||||
when(warehouse.resolvePallet("PAL-1")).thenReturn(resource(100L, null));
|
||||
when(warehouse.inspectPallet("PAL-1")).thenReturn(resource(100L, null));
|
||||
when(pallets.selectList(any())).thenReturn(List.of(planPallet()));
|
||||
doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "duplicate carton"))
|
||||
.when(workflow).validateCartonResolved(any(), any(), any(), any(), any());
|
||||
|
||||
+4
-4
@@ -61,7 +61,7 @@ class PdaInventoryMoveServiceTest {
|
||||
@Test
|
||||
void atomicallyRelocatesPhysicalPalletAndEveryWarehouseScopedPosition() {
|
||||
String targetLocationCode = "PDAOPS-ADMIN-API-20260825095854036-MOVE";
|
||||
when(warehouse.resolvePallet("PAL-1")).thenReturn(pallet(900L, 20L, 2));
|
||||
when(warehouse.inspectPallet("PAL-1")).thenReturn(pallet(900L, 20L, 2));
|
||||
when(warehouse.resolveLocation(900L, 20L)).thenReturn(location(20L, "A-01"));
|
||||
when(warehouse.resolveLocation(900L, targetLocationCode)).thenReturn(location(21L, targetLocationCode));
|
||||
InventoryPosition position = new InventoryPosition(); position.setId(11L); position.setCustomerId("100");
|
||||
@@ -101,7 +101,7 @@ class PdaInventoryMoveServiceTest {
|
||||
|
||||
@Test
|
||||
void rejectsCrossWarehousePalletWithoutMovingResources() {
|
||||
when(warehouse.resolvePallet("PAL-1")).thenReturn(pallet(901L, 20L, 2));
|
||||
when(warehouse.inspectPallet("PAL-1")).thenReturn(pallet(901L, 20L, 2));
|
||||
assertThatThrownBy(() -> service.move(900L, "operator-1",
|
||||
new PdaInventoryMoveRequest("PAL-1", "B-01", 2, "move-1", "库位优化")))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
|
||||
@@ -110,7 +110,7 @@ class PdaInventoryMoveServiceTest {
|
||||
|
||||
@Test
|
||||
void missingSourcePalletIsReportedAsNotFound() {
|
||||
when(warehouse.resolvePallet("MISSING")).thenReturn(null);
|
||||
when(warehouse.inspectPallet("MISSING")).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> service.move(900L, "operator-1",
|
||||
new PdaInventoryMoveRequest("MISSING", "B-01", 2, "move-1", "库位优化")))
|
||||
@@ -120,7 +120,7 @@ class PdaInventoryMoveServiceTest {
|
||||
|
||||
@Test
|
||||
void resumesWhenWarehouseCommittedButLastlegTransactionRolledBack() {
|
||||
when(warehouse.resolvePallet("PAL-1")).thenReturn(pallet(900L, 21L, 3));
|
||||
when(warehouse.inspectPallet("PAL-1")).thenReturn(pallet(900L, 21L, 3));
|
||||
when(warehouse.resolveLocation(900L, "B-01")).thenReturn(location(21L, "B-01"));
|
||||
when(warehouse.resolveLocation(900L, 20L)).thenReturn(location(20L, "A-01"));
|
||||
InventoryPosition position = new InventoryPosition(); position.setId(11L); position.setCustomerId("100");
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ class PdaStocktakeServiceTest {
|
||||
|
||||
@Test
|
||||
void rejectsPalletFromAnotherWarehouseBeforeReadingInventory() {
|
||||
when(warehouse.resolvePallet("PAL-1")).thenReturn(new WarehouseResourceGateway.WarehousePalletReference(
|
||||
when(warehouse.inspectPallet("PAL-1")).thenReturn(new WarehouseResourceGateway.WarehousePalletReference(
|
||||
30L, "PAL-1", 901L, 20L, "OCCUPIED", null, 2));
|
||||
|
||||
assertThatThrownBy(() -> service.create(900L, "operator-1",
|
||||
|
||||
@@ -59,11 +59,11 @@ const MODULE_DEPENDENCIES = new Map([
|
||||
['truck-appointments', ['merchant-qualification-fixture']],
|
||||
['service-center-flow', ['ocean-bookings', 'truck-appointments']],
|
||||
['phase3-loading-finalize', ['phase3-loading', 'phase3-evidence-gate']],
|
||||
['phase3-container-documents', ['phase3-loading-finalize']],
|
||||
['phase3-container-documents', ['phase3-outbound-dispatch']],
|
||||
['phase3-customs', ['phase3-loading-finalize']],
|
||||
['phase3-container-customs', ['merchant-qualification-fixture']],
|
||||
['phase3-port-schedules', ['ocean-bookings']],
|
||||
['phase3-outbound-dispatch', ['phase3-container-documents', 'phase3-customs', 'phase3-port-schedules', 'service-center-flow']],
|
||||
['phase3-outbound-dispatch', ['phase3-customs', 'phase3-port-schedules', 'service-center-flow']],
|
||||
['phase3-outbound-aggregate', ['phase3-outbound-dispatch']],
|
||||
['phase3-dispatch-gate', ['phase3-outbound-dispatch']],
|
||||
['phase3-waiting-for-loading', ['phase3-outbound-dispatch']],
|
||||
|
||||
@@ -134,6 +134,20 @@ function inventorySignature(snapshot) {
|
||||
})))
|
||||
}
|
||||
|
||||
function channelGroup(channelCode) {
|
||||
const value = String(channelCode || '').toUpperCase()
|
||||
if (value.startsWith('EXX_')) return 'EXX'
|
||||
if (value.startsWith('MATSON_CLX_')) return 'MATSON_CLX'
|
||||
if (value.startsWith('MATSON_MAX_')) return 'MATSON_MAX'
|
||||
if (value.startsWith('HEDE_')) return 'HEDE'
|
||||
if (value.startsWith('OA_LA_') || value === 'OA_PARCEL') return 'OA_LA'
|
||||
if (value.startsWith('OA_CHICAGO_')) return 'OA_CHICAGO'
|
||||
if (value.startsWith('OA_HOUSTON_')) return 'OA_HOUSTON'
|
||||
if (value.startsWith('OA_SAVANNAH_')) return 'OA_SAVANNAH'
|
||||
if (value === 'OA_NEW_YORK_TRUCK') return value
|
||||
throw new Error(`unsupported smart-loading channel group: ${channelCode}`)
|
||||
}
|
||||
|
||||
function operationsSignature(operations) {
|
||||
return JSON.stringify((operations || []).map((operation) => ({
|
||||
id: id(operation?.id), operationId: operation?.operationId || null,
|
||||
@@ -225,12 +239,25 @@ export async function run(context) {
|
||||
const candidates = context.apiSuccess(MODULE, '查询本轮入库单柜次装柜候选', candidateResponse, '', OPTIONS)
|
||||
const inboundCandidates = Array.isArray(candidates) ? candidates.filter((item) => id(item.inboundId) === inboundId
|
||||
&& Number(item.availablePackageCount) > 0) : []
|
||||
const candidate = inboundCandidates[0]
|
||||
let candidate = inboundCandidates[0]
|
||||
let dropshipCandidates = []
|
||||
if (source?.fulfillmentType === 'ONE_CLICK_DROPSHIP') {
|
||||
const group = channelGroup(initial?.summary?.channelCode)
|
||||
const candidatePath = `${smartLoadingBase}/inbound-candidates?channelGroupCodes=${encodeURIComponent(group)}&warehouseId=${encodeURIComponent(warehouseId)}&customerId=${encodeURIComponent(tenantId)}&inboundCode=${encodeURIComponent(initial?.summary?.inboundCode || source?.inboundCode || '')}&orderStatuses=WAIT_SPLIT&page=1&size=20`
|
||||
const palletCandidatesResponse = await context.request('GET', candidatePath)
|
||||
const palletCandidatePage = context.apiSuccess(MODULE, '查询一件代发整托装柜候选', palletCandidatesResponse, '', OPTIONS)
|
||||
dropshipCandidates = Array.isArray(palletCandidatePage?.records)
|
||||
? palletCandidatePage.records.filter((item) => id(item.inboundId) === inboundId
|
||||
&& id(item.palletId) && item.unitType === 'SEALED_PALLET' && item.indivisible === true) : []
|
||||
candidate = dropshipCandidates[0]
|
||||
context.assert(MODULE, '一件代发候选只按已封托托盘返回', palletCandidatesResponse,
|
||||
dropshipCandidates.length > 0, `inboundId=${inboundId}, pallets=${dropshipCandidates.length}`, OPTIONS)
|
||||
}
|
||||
context.assert(MODULE, '柜次装柜候选与入库单、客户和仓库一致', candidateResponse,
|
||||
Boolean(candidate?.inboundDetailId) && id(candidate?.accountId) === tenantId
|
||||
&& id(candidate?.warehouseId) === warehouseId,
|
||||
`candidate=${Boolean(candidate)}, warehouseId=${warehouseId || 'missing'}`, OPTIONS)
|
||||
if (!candidate?.inboundDetailId || !warehouseId) return
|
||||
if ((!candidate?.inboundDetailId && !id(candidate?.palletId)) || !warehouseId) return
|
||||
|
||||
|
||||
const loadingPlanResponse = await request(context, 'GET',
|
||||
@@ -240,7 +267,7 @@ export async function run(context) {
|
||||
if (!loadingPlanId) return
|
||||
const dropshipAssignment = source?.fulfillmentType === 'ONE_CLICK_DROPSHIP'
|
||||
const initialAssignmentCount = dropshipAssignment
|
||||
? inboundCandidates.filter((item) => id(item.palletId) === id(candidate.palletId))
|
||||
? dropshipCandidates.filter((item) => id(item.palletId) === id(candidate.palletId))
|
||||
.reduce((total, item) => total + Number(item.availablePackageCount || 0), 0) : 1
|
||||
const assignmentPath = dropshipAssignment
|
||||
? `${smartLoadingBase}/plans/${loadingPlanId}/pallet-assignments`
|
||||
|
||||
@@ -503,6 +503,9 @@ test('PDA completion acceptance verifies immutable BATCH and COMPLETE_LOADING au
|
||||
}
|
||||
assert.match(loading, /expectedBatchOperationIds = dropshipWholePallet[\s\S]*\? \[batchKey\][\s\S]*: \[batchKey, skipBatchKey, partialBatchKey, dynamicBatchKey\]/)
|
||||
assert.match(loading, /dropshipAssignment[\s\S]*pallet-assignments/)
|
||||
assert.match(loading, /inbound-candidates\?channelGroupCodes=/)
|
||||
assert.match(loading, /unitType === 'SEALED_PALLET'/)
|
||||
assert.match(loading, /indivisible === true/)
|
||||
assert.match(loading, /palletId: id\(candidate\.palletId\)/)
|
||||
assert.doesNotMatch(loading, /bridge-assign-inbound/)
|
||||
assert.match(loading, /skipBatchVerified, partialBatchVerified, dynamicIncrementVerified/)
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ test('钱包闭环覆盖充值、授信、采购冻结结算和换标结算释
|
||||
'sourceInventoryBalanceId:',
|
||||
'sourceInventoryPositionId:',
|
||||
'sourceInboundOrderId:',
|
||||
'destinationFbaWarehouseCode: source.destinationFbaWarehouseCode',
|
||||
'cartonMark:',
|
||||
'fbaCartonMark:',
|
||||
]) assert.ok(source.includes(token), token)
|
||||
|
||||
@@ -183,7 +183,8 @@ function relabelPayload(context, warehouseId, source, suffix, chargeAmount) {
|
||||
totalPackageCount: 1, notifiedPackageCount: 1, shipmentNo: `SHIP-${value}-${suffix}`.slice(0, 128),
|
||||
newSkuCode: `NEW-${value}-${suffix}`.slice(0, 128), labelRequirement: context.marker,
|
||||
cartonMark: `CM-${value}-${suffix}`.slice(0, 255),
|
||||
fbaCartonMark: `FBA-${value}-${suffix}`.slice(0, 255) }],
|
||||
fbaCartonMark: `FBA-${value}-${suffix}`.slice(0, 255),
|
||||
destinationFbaWarehouseCode: source.destinationFbaWarehouseCode }],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +196,7 @@ async function relabelFlow(context, overseasToken, warehouseId, suffix, terminal
|
||||
? candidatesResponse.body.data : []
|
||||
const source = candidates.find((candidate) => id(candidate.inboundOrderId)
|
||||
&& id(candidate.inventoryBalanceId)
|
||||
&& id(candidate.destinationFbaWarehouseCode)
|
||||
&& candidate.positions?.some((position) => id(position.positionId) && Number(position.availablePackages) > 0))
|
||||
if (source) source.position = source.positions.find((position) => id(position.positionId)
|
||||
&& Number(position.availablePackages) > 0)
|
||||
|
||||
@@ -165,6 +165,8 @@ test('PDA entry expands the real phase-three producer chain and builds an FBA/OV
|
||||
const variant = source.slice(source.indexOf('async runPhase4ProducerVariant'), source.indexOf('async ensurePhase4PdaProducerMatrix'))
|
||||
assert.ok(variant.indexOf('phase3-outbound-dispatch-phase4-') < variant.indexOf('phase3-container-documents-phase4-'))
|
||||
assert.ok(variant.indexOf('phase3-container-documents-phase4-') < variant.indexOf('phase3-carrier-schedule-queries-phase4-'))
|
||||
assert.match(source, /\['phase3-container-documents', \['phase3-outbound-dispatch'\]\]/)
|
||||
assert.doesNotMatch(source, /\['phase3-outbound-dispatch', \['phase3-container-documents'/)
|
||||
assert.match(source, /carrierScheduleQueryId/)
|
||||
assert.match(source, /readyForPhase4Pda === true/)
|
||||
assert.equal(modules.indexOf('phase4-overseas-pda-operations'), modules.indexOf('phase4-overseas-pda') + 1)
|
||||
|
||||
Reference in New Issue
Block a user