test(firstleg): decouple PDA completion from truck
This commit is contained in:
@@ -40,6 +40,8 @@ import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
import org.mockito.InOrder;
|
import org.mockito.InOrder;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
@@ -785,6 +787,7 @@ class LoadingPickServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void failedSameOperationIdCanResumeWithoutCreatingAnotherCommand() {
|
void failedSameOperationIdCanResumeWithoutCreatingAnotherCommand() {
|
||||||
|
assertNoTruckCoupling();
|
||||||
LoadingPickTaskEntity failed = task();
|
LoadingPickTaskEntity failed = task();
|
||||||
failed.setTaskStatus("COMPLETION_FAILED");
|
failed.setTaskStatus("COMPLETION_FAILED");
|
||||||
LoadingPickTaskEntity completing = task();
|
LoadingPickTaskEntity completing = task();
|
||||||
@@ -871,6 +874,35 @@ class LoadingPickServiceTest {
|
|||||||
verify(completionCommands).complete(900L, "worker-1");
|
verify(completionCommands).complete(900L, "worker-1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "truck state {0} never participates in PDA completion")
|
||||||
|
@ValueSource(strings = {"NO_APPOINTMENT", "DRAFT", "IN_PROGRESS", "COMPLETED"})
|
||||||
|
void completionWorkerIsIndependentOfEveryTruckAppointmentState(String truckState) {
|
||||||
|
assertNoTruckCoupling();
|
||||||
|
LoadingPickTaskEntity completing = task();
|
||||||
|
completing.setTaskStatus("COMPLETING");
|
||||||
|
when(repository.task(20L)).thenReturn(completing);
|
||||||
|
when(repository.inboundOwners(20L)).thenReturn(List.of(
|
||||||
|
new LoadingPickItemMapper.InboundOwner(101L, 50L)));
|
||||||
|
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
|
||||||
|
when(repository.review(20L)).thenReturn(null);
|
||||||
|
when(repository.approve(20L, 9L, "operator", "checked")).thenReturn(1);
|
||||||
|
when(repository.completeAccepted(20L, 9L, "operator")).thenReturn(1);
|
||||||
|
long commandId = 920L + Math.abs(truckState.hashCode() % 20);
|
||||||
|
when(completionCommands.complete(commandId, "worker-truck-independent")).thenReturn(true);
|
||||||
|
|
||||||
|
service.processCompletion(completionCommand(commandId,
|
||||||
|
"async-truck-independent-" + truckState.toLowerCase()), "worker-truck-independent");
|
||||||
|
|
||||||
|
verify(cabinetEvidenceService).requireComplete(99L);
|
||||||
|
verify(settlementService).settleOwner(20L, 50L, 9L, "operator");
|
||||||
|
verify(loadingDeliveryService).completeAfterPdaSettlement(101L, 50L, 9L, "operator",
|
||||||
|
"PDA-async-truck-independent-" + truckState.toLowerCase() + "-50", "checked");
|
||||||
|
verify(loadingListService).finishFromPda(10L, 20L, 9L, "operator");
|
||||||
|
verify(loadingListService).publishCompletedEvent(10L);
|
||||||
|
verify(repository).completeAccepted(20L, 9L, "operator");
|
||||||
|
verify(completionCommands).complete(commandId, "worker-truck-independent");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void workerUsesCabinetEvidenceOnceWithReversedTwoCustomerOwnerOrder() {
|
void workerUsesCabinetEvidenceOnceWithReversedTwoCustomerOwnerOrder() {
|
||||||
LoadingPickTaskEntity completing = task();
|
LoadingPickTaskEntity completing = task();
|
||||||
@@ -911,6 +943,7 @@ class LoadingPickServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void secondCustomerOwnerReadyFailureLeavesEveryOwnerUnsettledAndUncompleted() {
|
void secondCustomerOwnerReadyFailureLeavesEveryOwnerUnsettledAndUncompleted() {
|
||||||
|
assertNoTruckCoupling();
|
||||||
LoadingPickTaskEntity completing = task();
|
LoadingPickTaskEntity completing = task();
|
||||||
completing.setTaskStatus("COMPLETING");
|
completing.setTaskStatus("COMPLETING");
|
||||||
when(repository.task(20L)).thenReturn(completing);
|
when(repository.task(20L)).thenReturn(completing);
|
||||||
@@ -939,6 +972,7 @@ class LoadingPickServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void workerRestartResumesFromOwnerCheckpointsWithoutRepeatingSettlements() {
|
void workerRestartResumesFromOwnerCheckpointsWithoutRepeatingSettlements() {
|
||||||
|
assertNoTruckCoupling();
|
||||||
LoadingPickTaskEntity completing = task();
|
LoadingPickTaskEntity completing = task();
|
||||||
completing.setTaskStatus("COMPLETING");
|
completing.setTaskStatus("COMPLETING");
|
||||||
when(repository.task(20L)).thenReturn(completing);
|
when(repository.task(20L)).thenReturn(completing);
|
||||||
@@ -1180,6 +1214,14 @@ class LoadingPickServiceTest {
|
|||||||
verify(repository, never()).touchTask(any(Long.class), any(Integer.class));
|
verify(repository, never()).touchTask(any(Long.class), any(Integer.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void assertNoTruckCoupling() {
|
||||||
|
assertThat(LoadingPickService.class.getDeclaredFields())
|
||||||
|
.noneMatch(field -> field.getType().equals(TruckAppointmentService.class));
|
||||||
|
assertThat(LoadingPickService.class.getDeclaredMethods())
|
||||||
|
.noneMatch(method -> method.getName().equals("completeTruckAfterCommit")
|
||||||
|
|| method.getName().equals("completeLoadingFromPda"));
|
||||||
|
}
|
||||||
|
|
||||||
private void preparePick(LoadingPickTaskEntity task, LoadingPickItemEntity item, String operationId) {
|
private void preparePick(LoadingPickTaskEntity task, LoadingPickItemEntity item, String operationId) {
|
||||||
when(repository.history(operationId)).thenReturn(null);
|
when(repository.history(operationId)).thenReturn(null);
|
||||||
when(repository.task(20L)).thenReturn(task);
|
when(repository.task(20L)).thenReturn(task);
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ test('HTTP chain refreshes after stale inventory and keeps one stable PDA identi
|
|||||||
assert.doesNotMatch(loading, /\/loading\/complete\?tenantId=/)
|
assert.doesNotMatch(loading, /\/loading\/complete\?tenantId=/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('downstream finalizer uses the frozen truck-coupled complete contract', () => {
|
test('downstream finalizer uses the frozen loading complete contract without truck coupling', () => {
|
||||||
assert.match(convergence, /loadingPlan\?\.status === 'LOADING'/)
|
assert.match(convergence, /loadingPlan\?\.status === 'LOADING'/)
|
||||||
assert.match(convergence, /!operations\.some\(\(item\) => item\.actionType === 'COMPLETE_LOADING'\)/)
|
assert.match(convergence, /!operations\.some\(\(item\) => item\.actionType === 'COMPLETE_LOADING'\)/)
|
||||||
assert.match(customs, /\['AWAITING_REVIEW', 'LOADED'\]/)
|
assert.match(customs, /\['AWAITING_REVIEW', 'LOADED'\]/)
|
||||||
@@ -136,6 +136,8 @@ test('downstream finalizer uses the frozen truck-coupled complete contract', ()
|
|||||||
assert.doesNotMatch(finalize, /BLOCKED_CONTRACT/)
|
assert.doesNotMatch(finalize, /BLOCKED_CONTRACT/)
|
||||||
assert.doesNotMatch(finalize, /truck\?\.finalStatus === 'COMPLETED'/)
|
assert.doesNotMatch(finalize, /truck\?\.finalStatus === 'COMPLETED'/)
|
||||||
assert.doesNotMatch(finalize, /\/loading\/complete\?tenantId=/)
|
assert.doesNotMatch(finalize, /\/loading\/complete\?tenantId=/)
|
||||||
|
assert.doesNotMatch(loadingPickService,
|
||||||
|
/TruckAppointmentService|completeLoadingFromPda|completeTruckAfterCommit/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('PDA complete OpenAPI freezes reliable async acceptance and checkpoint recovery', () => {
|
test('PDA complete OpenAPI freezes reliable async acceptance and checkpoint recovery', () => {
|
||||||
@@ -215,11 +217,12 @@ test('worker final Firstleg convergence is one short transaction after remote ow
|
|||||||
'settlementService.settleOwner(',
|
'settlementService.settleOwner(',
|
||||||
'loadingDeliveryService.completeAfterPdaSettlement(',
|
'loadingDeliveryService.completeAfterPdaSettlement(',
|
||||||
'completionTransaction.executeWithoutResult',
|
'completionTransaction.executeWithoutResult',
|
||||||
'completeTruckAfterCommit(',
|
|
||||||
])
|
])
|
||||||
const finalizer = loadingPickService.slice(loadingPickService.indexOf('private void finalizeCompletion'),
|
const finalizer = loadingPickService.slice(loadingPickService.indexOf('private void finalizeCompletion'),
|
||||||
loadingPickService.indexOf('private void requireColoadConservation'))
|
loadingPickService.indexOf('private void requireColoadConservation'))
|
||||||
for (const token of ['COMPLETE_LOADING', 'finishFromPda', 'syncInboundProjections',
|
for (const token of ['COMPLETE_LOADING', 'finishFromPda', 'syncInboundProjections',
|
||||||
'publishCompletedEvent', 'completeAccepted', 'completionCommands.complete']) assert.match(finalizer, new RegExp(token))
|
'publishCompletedEvent', 'completeAccepted', 'completionCommands.complete']) assert.match(finalizer, new RegExp(token))
|
||||||
assert.doesNotMatch(finalizer, /settlementService|completeTruckAfterCommit/)
|
assert.doesNotMatch(finalizer, /settlementService/)
|
||||||
|
assert.doesNotMatch(loadingPickService,
|
||||||
|
/TruckAppointmentService|completeLoadingFromPda|completeTruckAfterCommit/)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -917,6 +917,9 @@ export async function run(context) {
|
|||||||
acceptedComplete.http === 200 && codeOf(acceptedComplete) === '200'
|
acceptedComplete.http === 200 && codeOf(acceptedComplete) === '200'
|
||||||
&& ['PROCESSING', 'FAILED', 'COMPLETED'].includes(acceptedComplete.body?.data?.workflowStatus),
|
&& ['PROCESSING', 'FAILED', 'COMPLETED'].includes(acceptedComplete.body?.data?.workflowStatus),
|
||||||
`${responseEvidence(acceptedComplete)}, workflow=${acceptedComplete.body?.data?.workflowStatus || 'missing'}`, OPTIONS)
|
`${responseEvidence(acceptedComplete)}, workflow=${acceptedComplete.body?.data?.workflowStatus || 'missing'}`, OPTIONS)
|
||||||
|
context.assert(MODULE, '独立柜未创建拖车单也不阻塞 PDA 装柜完成命令', acceptedComplete,
|
||||||
|
acceptedComplete.http === 200 && codeOf(acceptedComplete) === '200',
|
||||||
|
`containerBookingId=${containerBookingId}, oceanBookingId=${bookingId}, ${responseEvidence(acceptedComplete)}`, OPTIONS)
|
||||||
|
|
||||||
let terminalTask = acceptedComplete.body?.data
|
let terminalTask = acceptedComplete.body?.data
|
||||||
let terminalResponse = acceptedComplete
|
let terminalResponse = acceptedComplete
|
||||||
|
|||||||
@@ -140,6 +140,22 @@ test('late container gate mode proves the final gate without requiring evidence
|
|||||||
assert.match(value, /nextStage === 'CABINET_EVIDENCE' && nextCode === 'A0409'/)
|
assert.match(value, /nextStage === 'CABINET_EVIDENCE' && nextCode === 'A0409'/)
|
||||||
assert.match(value, /!nextMessage\.includes\('集装箱号'\)/)
|
assert.match(value, /!nextMessage\.includes\('集装箱号'\)/)
|
||||||
assert.match(value, /context\.createdResources\.phase3LateContainerGate/)
|
assert.match(value, /context\.createdResources\.phase3LateContainerGate/)
|
||||||
|
assert.match(value, /独立柜未创建拖车单也不阻塞 PDA 装柜完成命令/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('PDA completion production and HTTP chain never call or gate on truck appointments', () => {
|
||||||
|
const service = source('../../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java')
|
||||||
|
const serviceTest = source('../../firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java')
|
||||||
|
const loading = source('./phase3-loading.mjs')
|
||||||
|
assert.doesNotMatch(service, /TruckAppointmentService|completeLoadingFromPda|completeTruckAfterCommit/)
|
||||||
|
assert.match(serviceTest, /completionWorkerIsIndependentOfEveryTruckAppointmentState/)
|
||||||
|
for (const state of ['NO_APPOINTMENT', 'DRAFT', 'IN_PROGRESS', 'COMPLETED']) {
|
||||||
|
assert.match(serviceTest, new RegExp(state))
|
||||||
|
}
|
||||||
|
assert.match(serviceTest, /failedSameOperationIdCanResumeWithoutCreatingAnotherCommand[\s\S]*assertNoTruckCoupling/)
|
||||||
|
assert.match(serviceTest, /secondCustomerOwnerReadyFailureLeavesEveryOwnerUnsettledAndUncompleted[\s\S]*assertNoTruckCoupling/)
|
||||||
|
assert.match(serviceTest, /workerRestartResumesFromOwnerCheckpointsWithoutRepeatingSettlements[\s\S]*assertNoTruckCoupling/)
|
||||||
|
assert.doesNotMatch(loading, /\/truck-appointments|completeLoadingFromPda/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('push replay and both conflict modes preserve the complete pre-PDA snapshot', () => {
|
test('push replay and both conflict modes preserve the complete pre-PDA snapshot', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user