fix(acceptance): complete overseas fulfillment closure
This commit is contained in:
@@ -2020,9 +2020,9 @@ CREATE TABLE IF NOT EXISTS ll_container_plan_conversion_command (
|
|||||||
idempotency_key VARCHAR(128) NOT NULL COMMENT '首次调用幂等键', request_hash CHAR(64) NOT NULL COMMENT '确认请求SHA-256摘要',
|
idempotency_key VARCHAR(128) NOT NULL COMMENT '首次调用幂等键', request_hash CHAR(64) NOT NULL COMMENT '确认请求SHA-256摘要',
|
||||||
snapshot_version BIGINT NOT NULL COMMENT '确认的柜级快照版本', snapshot_hash CHAR(64) NOT NULL COMMENT '确认的柜级快照SHA-256',
|
snapshot_version BIGINT NOT NULL COMMENT '确认的柜级快照版本', snapshot_hash CHAR(64) NOT NULL COMMENT '确认的柜级快照SHA-256',
|
||||||
warehouse_id BIGINT NOT NULL COMMENT '海外接柜仓ID', batch_key VARCHAR(160) NOT NULL COMMENT '海外仓托盘批次幂等键',
|
warehouse_id BIGINT NOT NULL COMMENT '海外接柜仓ID', batch_key VARCHAR(160) NOT NULL COMMENT '海外仓托盘批次幂等键',
|
||||||
status VARCHAR(32) NOT NULL COMMENT 'PREPARED/APPLYING/LOCAL_COMMITTED/COMPLETED', preparation_json JSON NOT NULL COMMENT '稳定计划号与托盘业务键快照',
|
status VARCHAR(32) NOT NULL COMMENT '转换状态:PREPARED、APPLYING、LOCAL_COMMITTED或COMPLETED', preparation_json JSON NOT NULL COMMENT '稳定计划号与托盘业务键快照',
|
||||||
result_json JSON NULL COMMENT '本地计划提交结果', last_error VARCHAR(1000) NULL COMMENT '最近一次跨服务错误',
|
result_json JSON NULL COMMENT '本地计划提交结果', last_error VARCHAR(1000) NULL COMMENT '最近一次跨服务错误',
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_container_conversion_command(source_container_booking_id),
|
PRIMARY KEY(id), UNIQUE KEY uk_ll_container_conversion_command(source_container_booking_id),
|
||||||
UNIQUE KEY uk_ll_container_conversion_batch(warehouse_id,batch_key), KEY idx_ll_container_conversion_status(status,updated_at)
|
UNIQUE KEY uk_ll_container_conversion_batch(warehouse_id,batch_key), KEY idx_ll_container_conversion_status(status,updated_at)
|
||||||
) ENGINE=InnoDB COMMENT='柜级入库计划转换Saga与幂等命令';
|
) ENGINE=InnoDB COMMENT='柜级入库计划转换Saga与幂等命令';
|
||||||
|
|||||||
+2
-1
@@ -300,7 +300,8 @@ public class PdaPalletOperationService {
|
|||||||
if (unbind && changes.stream().noneMatch(change -> "IDLE".equals(change.targetStatus()))) {
|
if (unbind && changes.stream().noneMatch(change -> "IDLE".equals(change.targetStatus()))) {
|
||||||
throw conflict("解绑必须清空来源托盘");
|
throw conflict("解绑必须清空来源托盘");
|
||||||
}
|
}
|
||||||
changes.sort(Comparator.comparingLong(change -> change.pallet().palletId()));
|
changes.sort(Comparator.comparingInt((PalletChange change) -> "IDLE".equals(change.targetStatus()) ? 0 : 1)
|
||||||
|
.thenComparingLong(change -> change.pallet().palletId()));
|
||||||
return new Plan(pallets, moves, changes);
|
return new Plan(pallets, moves, changes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -589,12 +589,12 @@ CREATE TABLE IF NOT EXISTS ll_container_plan_conversion_command (
|
|||||||
snapshot_hash CHAR(64) NOT NULL COMMENT '确认的柜级快照SHA-256',
|
snapshot_hash CHAR(64) NOT NULL COMMENT '确认的柜级快照SHA-256',
|
||||||
warehouse_id BIGINT NOT NULL COMMENT '海外接柜仓ID',
|
warehouse_id BIGINT NOT NULL COMMENT '海外接柜仓ID',
|
||||||
batch_key VARCHAR(160) NOT NULL COMMENT '海外仓托盘批次幂等键',
|
batch_key VARCHAR(160) NOT NULL COMMENT '海外仓托盘批次幂等键',
|
||||||
status VARCHAR(32) NOT NULL COMMENT 'PREPARED/APPLYING/LOCAL_COMMITTED/COMPLETED',
|
status VARCHAR(32) NOT NULL COMMENT '转换状态:PREPARED、APPLYING、LOCAL_COMMITTED或COMPLETED',
|
||||||
preparation_json JSON NOT NULL COMMENT '稳定计划号与托盘业务键快照',
|
preparation_json JSON NOT NULL COMMENT '稳定计划号与托盘业务键快照',
|
||||||
result_json JSON NULL COMMENT '本地计划提交结果',
|
result_json JSON NULL COMMENT '本地计划提交结果',
|
||||||
last_error VARCHAR(1000) NULL COMMENT '最近一次跨服务错误',
|
last_error VARCHAR(1000) NULL COMMENT '最近一次跨服务错误',
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
PRIMARY KEY(id),
|
PRIMARY KEY(id),
|
||||||
UNIQUE KEY uk_ll_container_conversion_command(source_container_booking_id),
|
UNIQUE KEY uk_ll_container_conversion_command(source_container_booking_id),
|
||||||
UNIQUE KEY uk_ll_container_conversion_batch(warehouse_id,batch_key),
|
UNIQUE KEY uk_ll_container_conversion_batch(warehouse_id,batch_key),
|
||||||
|
|||||||
+14
-8
@@ -18,6 +18,7 @@ import org.junit.jupiter.api.BeforeEach;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.InOrder;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
@@ -74,19 +75,21 @@ class PdaPalletOperationServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test void localConcurrencyFailureCompensatesEveryAppliedPhysicalTransitionInReverseSafeForm() {
|
@Test void localConcurrencyFailureCompensatesEveryAppliedPhysicalTransitionInReverseSafeForm() {
|
||||||
var source = pallet(10L, "SOURCE", 88L, 1);
|
var source = new PalletPhysicalGateway.PalletReference(20L, "SOURCE", 88L, 1020L,
|
||||||
var target = pallet(20L, "TARGET", 88L, 2);
|
"OCCUPIED", null, 1);
|
||||||
|
var target = new PalletPhysicalGateway.PalletReference(10L, "TARGET", 88L, 1010L,
|
||||||
|
"IDLE", null, 2);
|
||||||
when(physical.resolve("SOURCE")).thenReturn(source);
|
when(physical.resolve("SOURCE")).thenReturn(source);
|
||||||
when(physical.resolve("TARGET")).thenReturn(target);
|
when(physical.resolve("TARGET")).thenReturn(target);
|
||||||
when(physical.resolveLocation(88L, target.locationId())).thenReturn(
|
when(physical.resolveLocation(88L, target.locationId())).thenReturn(
|
||||||
new PalletPhysicalGateway.LocationReference(target.locationId(), "LOC-" + target.locationId(),
|
new PalletPhysicalGateway.LocationReference(target.locationId(), "LOC-" + target.locationId(),
|
||||||
"STORAGE", "ACTIVE"));
|
"STORAGE", "ACTIVE"));
|
||||||
PalletInventoryRow planned = row(100L, 10L, 1L, 5L);
|
PalletInventoryRow planned = row(100L, 20L, 1L, 5L);
|
||||||
PalletInventoryRow stale = row(100L, 10L, 2L, 5L);
|
PalletInventoryRow stale = row(100L, 20L, 2L, 5L);
|
||||||
when(mapper.content("88", 10L)).thenReturn(List.of(planned));
|
when(mapper.content("88", 20L)).thenReturn(List.of(planned));
|
||||||
when(mapper.content("88", 20L)).thenReturn(List.of());
|
when(mapper.content("88", 10L)).thenReturn(List.of());
|
||||||
when(mapper.palletPackages("88", 10L)).thenReturn(5L);
|
when(mapper.palletPackages("88", 20L)).thenReturn(5L);
|
||||||
when(mapper.palletPackages("88", 20L)).thenReturn(0L);
|
when(mapper.palletPackages("88", 10L)).thenReturn(0L);
|
||||||
when(mapper.lockPosition("88", 100L)).thenReturn(stale);
|
when(mapper.lockPosition("88", 100L)).thenReturn(stale);
|
||||||
|
|
||||||
AtomicReference<PdaPalletOperation> operation = new AtomicReference<>();
|
AtomicReference<PdaPalletOperation> operation = new AtomicReference<>();
|
||||||
@@ -103,6 +106,9 @@ class PdaPalletOperationServiceTest {
|
|||||||
|
|
||||||
assertThrows(ResponseStatusException.class, () -> service.assemble(identity(), request));
|
assertThrows(ResponseStatusException.class, () -> service.assemble(identity(), request));
|
||||||
|
|
||||||
|
InOrder transitions = inOrder(physical);
|
||||||
|
transitions.verify(physical).apply(eq(88L), eq(source), eq("IDLE"), anyLong(), anyString(), any());
|
||||||
|
transitions.verify(physical).apply(eq(88L), eq(target), eq("OCCUPIED"), anyLong(), anyString(), any());
|
||||||
verify(physical, times(2)).compensate(eq(88L), any(PalletPhysicalGateway.Transition.class));
|
verify(physical, times(2)).compensate(eq(88L), any(PalletPhysicalGateway.Transition.class));
|
||||||
assertEquals("FAILED_COMPENSATED", operation.get().getStatus());
|
assertEquals("FAILED_COMPENSATED", operation.get().getStatus());
|
||||||
verify(positions, never()).updateById(any(com.baiyue.lastleg.inventory.entity.InventoryPosition.class));
|
verify(positions, never()).updateById(any(com.baiyue.lastleg.inventory.entity.InventoryPosition.class));
|
||||||
|
|||||||
@@ -1387,6 +1387,8 @@ export class AcceptanceRun {
|
|||||||
internalRemark: fixtureOrder.internalRemark,
|
internalRemark: fixtureOrder.internalRemark,
|
||||||
cargoRemark: fixtureOrder.cargoRemark,
|
cargoRemark: fixtureOrder.cargoRemark,
|
||||||
taxMode: fixtureOrder.taxMode || null,
|
taxMode: fixtureOrder.taxMode || null,
|
||||||
|
finalStatus: fixture.finalStatus,
|
||||||
|
processStatus: fixture.processStatus,
|
||||||
requireInventoryMutation: requireSharedOverflow,
|
requireInventoryMutation: requireSharedOverflow,
|
||||||
} : { marker: originalMarker, sourceMarker: fixtureMarker, failed: true }
|
} : { marker: originalMarker, sourceMarker: fixtureMarker, failed: true }
|
||||||
const incrementalMarker = `${originalMarker}-LOAD${suffix}-INC`
|
const incrementalMarker = `${originalMarker}-LOAD${suffix}-INC`
|
||||||
@@ -1468,7 +1470,7 @@ export class AcceptanceRun {
|
|||||||
const variantMarker = `${matrixMarker}-P4-${label}`
|
const variantMarker = `${matrixMarker}-P4-${label}`
|
||||||
const contextKeys = ['orderDestinationType', 'orderFulfillmentType', 'phase4ProducerMatrixMarker']
|
const contextKeys = ['orderDestinationType', 'orderFulfillmentType', 'phase4ProducerMatrixMarker']
|
||||||
const createdKeys = [
|
const createdKeys = [
|
||||||
'phase3Loading', 'phase3LoadingCompletedProducer', 'phase3ContainerDocuments', 'orchestrationFixtures', 'oceanBooking',
|
'phase3Loading', 'phase3LoadingCompletedProducer', 'phase3ContainerDocuments', 'orchestrationFixtures', 'inboundFees', 'oceanBooking',
|
||||||
'phase3CustomsSupplier', 'phase3Customs', 'serviceCenterFlow', 'portSchedule',
|
'phase3CustomsSupplier', 'phase3Customs', 'serviceCenterFlow', 'portSchedule',
|
||||||
'phase3OutboundDispatch', 'phase3CarrierScheduleQuery', 'phase4CarrierPickupReady',
|
'phase3OutboundDispatch', 'phase3CarrierScheduleQuery', 'phase4CarrierPickupReady',
|
||||||
]
|
]
|
||||||
@@ -1493,6 +1495,16 @@ export class AcceptanceRun {
|
|||||||
// run; repeating it for every producer is unsafe for a populated shared warehouse and can
|
// run; repeating it for every producer is unsafe for a populated shared warehouse and can
|
||||||
// become impossible when the bounded isolation guard refuses bulk location mutations.
|
// become impossible when the bounded isolation guard refuses bulk location mutations.
|
||||||
restoreFixture = await this.preparePhase3LoadingFixture('', false)
|
restoreFixture = await this.preparePhase3LoadingFixture('', false)
|
||||||
|
if (fulfillmentType === 'ONE_CLICK_DROPSHIP') {
|
||||||
|
const measuredFixture = this.createdResources.inboundMeasurement
|
||||||
|
this.createdResources.inboundMeasurement = this.createdResources.orchestrationFixtures?.phase3Loading
|
||||||
|
try {
|
||||||
|
const inboundFees = await import(new URL('./admin-test-env-modules/inbound-fees.mjs', import.meta.url))
|
||||||
|
await this.executeModule(`inbound-fees-phase4-${label}`, () => inboundFees.run(this))
|
||||||
|
} finally {
|
||||||
|
this.createdResources.inboundMeasurement = measuredFixture
|
||||||
|
}
|
||||||
|
}
|
||||||
const loading = await import(new URL('./admin-test-env-modules/phase3-loading.mjs', import.meta.url))
|
const loading = await import(new URL('./admin-test-env-modules/phase3-loading.mjs', import.meta.url))
|
||||||
await this.executeModule(`phase3-loading-phase4-${label}`, () => loading.run(this))
|
await this.executeModule(`phase3-loading-phase4-${label}`, () => loading.run(this))
|
||||||
const finalize = await import(new URL('./admin-test-env-modules/phase3-loading-finalize.mjs', import.meta.url))
|
const finalize = await import(new URL('./admin-test-env-modules/phase3-loading-finalize.mjs', import.meta.url))
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs'
|
|||||||
const source = readFileSync(new URL('./phase3-container-documents.mjs', import.meta.url), 'utf8')
|
const source = readFileSync(new URL('./phase3-container-documents.mjs', import.meta.url), 'utf8')
|
||||||
const entry = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
|
const entry = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
|
||||||
|
|
||||||
test('three generic container documents are frozen and customs stays on customs-v1', () => {
|
test('two container operational documents are frozen and customs stays on customs-v1', () => {
|
||||||
assert.deepEqual(DOCUMENT_TYPES, [
|
assert.deepEqual(DOCUMENT_TYPES, [
|
||||||
'DESTINATION_CLEARANCE', 'DELIVERY_ORDER', 'PICKUP_ORDER',
|
'DESTINATION_CLEARANCE', 'DELIVERY_ORDER',
|
||||||
])
|
])
|
||||||
for (const token of [
|
for (const token of [
|
||||||
'/clearance/document/upload', '/clearance/complete',
|
'/clearance/document/upload', '/clearance/complete',
|
||||||
@@ -28,11 +28,11 @@ test('document lifecycle is fail-closed, traceable and covers replay, hash confl
|
|||||||
]) assert.ok(source.includes(token), token)
|
]) assert.ok(source.includes(token), token)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('focused pickup-ready chain orders documents before outbound and candidate visibility', () => {
|
test('focused pickup-ready dependencies reach outbound and candidate visibility', () => {
|
||||||
const modules = normalizeModuleOrder(['phase4-overseas-pda'])
|
const modules = normalizeModuleOrder(['phase4-overseas-pda'])
|
||||||
const required = [
|
const required = [
|
||||||
'phase3-loading-finalize', 'phase3-container-documents', 'phase3-customs',
|
'phase3-loading-finalize', 'phase3-customs', 'phase3-outbound-dispatch',
|
||||||
'phase3-outbound-dispatch', 'phase3-carrier-schedule-queries',
|
'phase3-carrier-schedule-queries',
|
||||||
'phase4-carrier-pickup-ready', 'phase4-overseas-pda',
|
'phase4-carrier-pickup-ready', 'phase4-overseas-pda',
|
||||||
]
|
]
|
||||||
for (const module of required) assert.ok(modules.includes(module), module)
|
for (const module of required) assert.ok(modules.includes(module), module)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const OPTIONS = { scenario: 'phase3-container-operational-documents', deployment
|
|||||||
const CONTAINERS = '/api/platform/v1/containers'
|
const CONTAINERS = '/api/platform/v1/containers'
|
||||||
|
|
||||||
export const DOCUMENT_TYPES = Object.freeze([
|
export const DOCUMENT_TYPES = Object.freeze([
|
||||||
'DESTINATION_CLEARANCE', 'DELIVERY_ORDER', 'PICKUP_ORDER',
|
'DESTINATION_CLEARANCE', 'DELIVERY_ORDER',
|
||||||
])
|
])
|
||||||
|
|
||||||
function id(value) { return value == null ? '' : String(value) }
|
function id(value) { return value == null ? '' : String(value) }
|
||||||
@@ -114,7 +114,7 @@ export async function run(context) {
|
|||||||
readPath: `${CONTAINERS}/${containerId}/clearance`,
|
readPath: `${CONTAINERS}/${containerId}/clearance`,
|
||||||
completePath: `${CONTAINERS}/${containerId}/clearance/complete`,
|
completePath: `${CONTAINERS}/${containerId}/clearance/complete`,
|
||||||
},
|
},
|
||||||
...['DELIVERY_ORDER', 'PICKUP_ORDER'].map((type) => ({
|
...['DELIVERY_ORDER'].map((type) => ({
|
||||||
type,
|
type,
|
||||||
uploadPath: `${CONTAINERS}/${containerId}/documents/${type}/upload`,
|
uploadPath: `${CONTAINERS}/${containerId}/documents/${type}/upload`,
|
||||||
readPath: `${CONTAINERS}/${containerId}/documents/${type}`,
|
readPath: `${CONTAINERS}/${containerId}/documents/${type}`,
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ export async function run(context) {
|
|||||||
dropshipCandidates.length > 0, `inboundId=${inboundId}, pallets=${dropshipCandidates.length}`, OPTIONS)
|
dropshipCandidates.length > 0, `inboundId=${inboundId}, pallets=${dropshipCandidates.length}`, OPTIONS)
|
||||||
}
|
}
|
||||||
context.assert(MODULE, '柜次装柜候选与入库单、客户和仓库一致', candidateResponse,
|
context.assert(MODULE, '柜次装柜候选与入库单、客户和仓库一致', candidateResponse,
|
||||||
Boolean(candidate?.inboundDetailId) && id(candidate?.accountId) === tenantId
|
Boolean(candidate?.inboundDetailId || id(candidate?.palletId)) && id(candidate?.accountId) === tenantId
|
||||||
&& id(candidate?.warehouseId) === warehouseId,
|
&& id(candidate?.warehouseId) === warehouseId,
|
||||||
`candidate=${Boolean(candidate)}, warehouseId=${warehouseId || 'missing'}`, OPTIONS)
|
`candidate=${Boolean(candidate)}, warehouseId=${warehouseId || 'missing'}`, OPTIONS)
|
||||||
if ((!candidate?.inboundDetailId && !id(candidate?.palletId)) || !warehouseId) return
|
if ((!candidate?.inboundDetailId && !id(candidate?.palletId)) || !warehouseId) return
|
||||||
|
|||||||
@@ -85,10 +85,12 @@ test('measurement and loading pass destination identity into the LOADED producer
|
|||||||
assert.match(entry, /runPhase4ProducerVariant\('DROPSHIP-PRIMARY', 'FBA', 'ONE_CLICK_DROPSHIP', true\)/)
|
assert.match(entry, /runPhase4ProducerVariant\('DROPSHIP-PRIMARY', 'FBA', 'ONE_CLICK_DROPSHIP', true\)/)
|
||||||
assert.match(entry, /orderFulfillmentType/)
|
assert.match(entry, /orderFulfillmentType/)
|
||||||
assert.match(entry, /fulfillmentType: fixtureOrder\.fulfillmentType \|\| 'STANDARD'/)
|
assert.match(entry, /fulfillmentType: fixtureOrder\.fulfillmentType \|\| 'STANDARD'/)
|
||||||
|
assert.match(entry, /inbound-fees-phase4-/)
|
||||||
|
assert.match(entry, /createdResources\.inboundMeasurement = this\.createdResources\.orchestrationFixtures\?\.phase3Loading/)
|
||||||
assert.match(entry, /skuCodes: Array\.isArray\(fixtureOrder\.skuCodes\) \? fixtureOrder\.skuCodes : \[\]/)
|
assert.match(entry, /skuCodes: Array\.isArray\(fixtureOrder\.skuCodes\) \? fixtureOrder\.skuCodes : \[\]/)
|
||||||
assert.match(entry, /skuLines: Array\.isArray\(fixtureOrder\.skuLines\) \? fixtureOrder\.skuLines : \[\]/)
|
assert.match(entry, /skuLines: Array\.isArray\(fixtureOrder\.skuLines\) \? fixtureOrder\.skuLines : \[\]/)
|
||||||
const pda = read('phase4-overseas-pda.mjs')
|
const pda = read('phase4-overseas-pda.mjs')
|
||||||
assert.match(loading, /inbound-assignments/)
|
assert.match(loading, /pallet-assignments/)
|
||||||
assert.match(loading, /canonicalPickItems\.map/)
|
assert.match(loading, /canonicalPickItems\.map/)
|
||||||
assert.match(loading, /pickedPackageCount: Number\(item\.plannedPackageCount\)/)
|
assert.match(loading, /pickedPackageCount: Number\(item\.plannedPackageCount\)/)
|
||||||
assert.match(loading, /const dropshipWholePallet = source\?\.fulfillmentType === 'ONE_CLICK_DROPSHIP'/)
|
assert.match(loading, /const dropshipWholePallet = source\?\.fulfillmentType === 'ONE_CLICK_DROPSHIP'/)
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ test('phase-five module covers dispatch edit, transfer, PDA pick/depart and deli
|
|||||||
assert.match(source, /READY_TO_DEPART/)
|
assert.match(source, /READY_TO_DEPART/)
|
||||||
assert.match(source, /OUTBOUNDED/)
|
assert.match(source, /OUTBOUNDED/)
|
||||||
assert.match(source, /DELIVERING/)
|
assert.match(source, /DELIVERING/)
|
||||||
|
assert.match(source, /expectedVersion: deliveryVersion/)
|
||||||
|
assert.match(source, /status: 'DELIVERED', expectedVersion: completeVersion/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('phase-five module proves the client delivery create, list and nested detail contract', () => {
|
test('phase-five module proves the client delivery create, list and nested detail contract', () => {
|
||||||
|
|||||||
@@ -250,13 +250,30 @@ export async function run(context) {
|
|||||||
outbound.data?.outbound?.outboundNo === outboundNo && outbound.data?.outbound?.status === 'OUTBOUNDED',
|
outbound.data?.outbound?.outboundNo === outboundNo && outbound.data?.outbound?.status === 'OUTBOUNDED',
|
||||||
trace(outbound.response), OPTIONS)
|
trace(outbound.response), OPTIONS)
|
||||||
|
|
||||||
|
const deliveryBeforeStatus = await platform(context, 'GET', '读取派送状态更新即时版本',
|
||||||
|
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}`, { customerId }))
|
||||||
|
const deliveryVersion = Number(deliveryBeforeStatus.data?.order?.version)
|
||||||
|
context.assert(MODULE, '派送详情提供状态乐观锁版本', deliveryBeforeStatus.response,
|
||||||
|
Number.isInteger(deliveryVersion) && deliveryVersion >= 0,
|
||||||
|
`version=${deliveryBeforeStatus.data?.order?.version ?? 'missing'}; ${trace(deliveryBeforeStatus.response)}`, OPTIONS)
|
||||||
|
if (!Number.isInteger(deliveryVersion) || deliveryVersion < 0) return
|
||||||
const delivering = await platform(context, 'PUT', '派送单推进配送中',
|
const delivering = await platform(context, 'PUT', '派送单推进配送中',
|
||||||
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/status`, { customerId }),
|
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/status`, { customerId }),
|
||||||
{ status: 'DELIVERING', remark: `${context.marker}-delivering` })
|
{ status: 'DELIVERING', expectedVersion: deliveryVersion, remark: `${context.marker}-delivering` })
|
||||||
context.assert(MODULE, '派送状态进入DELIVERING', delivering.response,
|
context.assert(MODULE, '派送状态进入DELIVERING', delivering.response,
|
||||||
delivering.data?.deliveryStatus === 'DELIVERING', trace(delivering.response), OPTIONS)
|
delivering.data?.deliveryStatus === 'DELIVERING', trace(delivering.response), OPTIONS)
|
||||||
|
if (!success(delivering.response)) return
|
||||||
|
const deliveryBeforeComplete = await platform(context, 'GET', '读取派送完成即时版本',
|
||||||
|
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}`, { customerId }))
|
||||||
|
const completeVersion = Number(deliveryBeforeComplete.data?.order?.version)
|
||||||
|
context.assert(MODULE, '配送中详情版本已递增', deliveryBeforeComplete.response,
|
||||||
|
Number.isInteger(completeVersion) && completeVersion > deliveryVersion,
|
||||||
|
`before=${deliveryVersion}, after=${deliveryBeforeComplete.data?.order?.version ?? 'missing'}; ${trace(deliveryBeforeComplete.response)}`,
|
||||||
|
OPTIONS)
|
||||||
|
if (!Number.isInteger(completeVersion) || completeVersion <= deliveryVersion) return
|
||||||
const completed = await platform(context, 'POST', '确认派送完成并生成客户回访',
|
const completed = await platform(context, 'POST', '确认派送完成并生成客户回访',
|
||||||
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/complete`, { customerId }))
|
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/complete`, { customerId }),
|
||||||
|
{ status: 'DELIVERED', expectedVersion: completeVersion, remark: `${context.marker}-delivered` })
|
||||||
const visitId = id(completed.data?.visitId)
|
const visitId = id(completed.data?.visitId)
|
||||||
context.assert(MODULE, '派送完成生成PENDING回访记录', completed.response,
|
context.assert(MODULE, '派送完成生成PENDING回访记录', completed.response,
|
||||||
completed.data?.deliveryStatus === 'DELIVERED' && completed.data?.visitStatus === 'PENDING'
|
completed.data?.deliveryStatus === 'DELIVERED' && completed.data?.visitStatus === 'PENDING'
|
||||||
@@ -264,7 +281,8 @@ export async function run(context) {
|
|||||||
if (!success(completed.response) || !visitId) return
|
if (!success(completed.response) || !visitId) return
|
||||||
context.createdResources.phase5OverseasFulfillment.visitId = visitId
|
context.createdResources.phase5OverseasFulfillment.visitId = visitId
|
||||||
const completeReplay = await platform(context, 'POST', '确认派送完成重复调用',
|
const completeReplay = await platform(context, 'POST', '确认派送完成重复调用',
|
||||||
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/complete`, { customerId }))
|
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/complete`, { customerId }),
|
||||||
|
{ status: 'DELIVERED', expectedVersion: completeVersion, remark: `${context.marker}-delivered` })
|
||||||
context.assert(MODULE, '派送完成重放复用同一回访', completeReplay.response,
|
context.assert(MODULE, '派送完成重放复用同一回访', completeReplay.response,
|
||||||
completeReplay.data?.replayed === true && id(completeReplay.data?.visitId) === visitId,
|
completeReplay.data?.replayed === true && id(completeReplay.data?.visitId) === visitId,
|
||||||
trace(completeReplay.response), OPTIONS)
|
trace(completeReplay.response), OPTIONS)
|
||||||
|
|||||||
@@ -163,6 +163,8 @@ test('PDA entry expands the real phase-three producer chain and builds an FBA/OV
|
|||||||
assert.match(source, /runPhase4ProducerVariant\('SECONDARY', 'FBA', 'STANDARD', false\)/)
|
assert.match(source, /runPhase4ProducerVariant\('SECONDARY', 'FBA', 'STANDARD', false\)/)
|
||||||
assert.match(source, /runUntilPickupReady\(this\)/)
|
assert.match(source, /runUntilPickupReady\(this\)/)
|
||||||
const variant = source.slice(source.indexOf('async runPhase4ProducerVariant'), source.indexOf('async ensurePhase4PdaProducerMatrix'))
|
const variant = source.slice(source.indexOf('async runPhase4ProducerVariant'), source.indexOf('async ensurePhase4PdaProducerMatrix'))
|
||||||
|
assert.ok(variant.indexOf('inbound-fees-phase4-') < variant.indexOf('phase3-loading-phase4-'))
|
||||||
|
assert.match(variant, /fulfillmentType === 'ONE_CLICK_DROPSHIP'/)
|
||||||
assert.ok(variant.indexOf('phase3-outbound-dispatch-phase4-') < variant.indexOf('phase3-container-documents-phase4-'))
|
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.ok(variant.indexOf('phase3-container-documents-phase4-') < variant.indexOf('phase3-carrier-schedule-queries-phase4-'))
|
||||||
assert.match(source, /\['phase3-container-documents', \['phase3-outbound-dispatch'\]\]/)
|
assert.match(source, /\['phase3-container-documents', \['phase3-outbound-dispatch'\]\]/)
|
||||||
|
|||||||
Reference in New Issue
Block a user