fix(firstleg): make truck status informational for departure
This commit is contained in:
-3
@@ -24,7 +24,6 @@ import java.util.*;
|
||||
@Service
|
||||
public class OutboundDispatchService {
|
||||
private static final Set<String> REQUIRED_CONTRIBUTORS = Set.of("CUSTOMS_CLEARANCE", "SAILING_SCHEDULE");
|
||||
private static final Set<String> TRUCK_READY = Set.of("DEPARTED", "COMPLETED");
|
||||
|
||||
private final OutboundDispatchRepository repository;
|
||||
private final Map<String, OutboundDispatchGateContributor> contributors;
|
||||
@@ -187,8 +186,6 @@ public class OutboundDispatchService {
|
||||
status(pick), "COMPLETED"));
|
||||
items.add(item("OCEAN_BOOKING_RELEASED", "订舱已放舱且分配当前柜次",
|
||||
ocean != null && "RELEASED".equals(ocean.status()), status(ocean), "RELEASED"));
|
||||
items.add(item("TRUCK_DEPARTED", "关联拖车已离仓", truck != null && TRUCK_READY.contains(truck.status()),
|
||||
status(truck), "DEPARTED或COMPLETED"));
|
||||
boolean evidenceReady = evidence != null && evidence.evidenceCount() != null
|
||||
&& evidence.evidenceCount() >= 5 && Objects.equals(evidence.validSeal(), 1);
|
||||
items.add(item("LOADING_EVIDENCE_COMPLETE", "四步照片与铅封完整", evidenceReady,
|
||||
|
||||
@@ -99,8 +99,8 @@ components:
|
||||
loadingPlanStatus: {type: string}
|
||||
oceanBookingId: {type: integer, format: int64, nullable: true}
|
||||
oceanBookingStatus: {type: string, nullable: true}
|
||||
truckAppointmentId: {type: integer, format: int64, nullable: true}
|
||||
truckAppointmentStatus: {type: string, nullable: true}
|
||||
truckAppointmentId: {type: integer, format: int64, nullable: true, description: Reference-only truck appointment; absence never blocks manual outbound confirmation}
|
||||
truckAppointmentStatus: {type: string, nullable: true, description: Reference-only status; PENDING or any non-terminal state never affects gate.passed}
|
||||
dispatchStatus: {type: string, description: One visible container-level status}
|
||||
version: {type: integer, minimum: 0, description: One optimistic-lock version shared by all technical inbound rows}
|
||||
inbounds:
|
||||
|
||||
+37
@@ -44,6 +44,34 @@ class OutboundDispatchServiceTest {
|
||||
.extracting(GateItem::passed).containsExactly(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmAllowsMissingTruckAppointmentWhenEveryAuthoritativeGatePasses() {
|
||||
OutboundDispatchService service = serviceWithoutCarrier();
|
||||
stubContainer();
|
||||
when(repository.truck(22L, 44L)).thenReturn(null);
|
||||
stubConfirmMutation("no-truck");
|
||||
|
||||
Detail result = service.confirm(22L, 9L, "admin", "no-truck", new ActionRequest(0, null));
|
||||
|
||||
assertThat(result.aggregate().truckAppointmentId()).isNull();
|
||||
assertThat(result.aggregate().gate().passed()).isTrue();
|
||||
verify(repository, times(2)).confirm(anyLong(), eq(0), eq(9L), eq("admin"), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmAllowsPendingTruckAppointmentWhenEveryAuthoritativeGatePasses() {
|
||||
OutboundDispatchService service = serviceWithoutCarrier();
|
||||
stubContainer();
|
||||
when(repository.truck(22L, 44L)).thenReturn(new OutboundDispatchReadMapper.IdStatus(77L, "PENDING"));
|
||||
stubConfirmMutation("pending-truck");
|
||||
|
||||
Detail result = service.confirm(22L, 9L, "admin", "pending-truck", new ActionRequest(0, null));
|
||||
|
||||
assertThat(result.aggregate().truckAppointmentStatus()).isEqualTo("PENDING");
|
||||
assertThat(result.aggregate().gate().passed()).isTrue();
|
||||
verify(repository, times(2)).confirm(anyLong(), eq(0), eq(9L), eq("admin"), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailReturnsOneVersionAndAllInboundBindings() {
|
||||
OutboundDispatchService service = serviceWithoutCarrier();
|
||||
@@ -185,6 +213,15 @@ class OutboundDispatchServiceTest {
|
||||
when(repository.evidence(7L, 12L)).thenReturn(new OutboundDispatchReadMapper.Evidence(5, 1));
|
||||
}
|
||||
|
||||
private void stubConfirmMutation(String key) {
|
||||
when(repository.lockBindings(22L)).thenReturn(bindings());
|
||||
when(repository.dispatchLocked(7L, 11L, 22L)).thenReturn(state(31L, 11L, "READY_FOR_DISPATCH", 0));
|
||||
when(repository.dispatchLocked(7L, 12L, 22L)).thenReturn(state(32L, 12L, "READY_FOR_DISPATCH", 0));
|
||||
when(repository.claim(eq(9L), eq("CONFIRM_DISPATCH"), eq(key), anyString())).thenReturn(true);
|
||||
when(repository.confirm(anyLong(), eq(0), eq(9L), eq("admin"), isNull())).thenReturn(1);
|
||||
when(repository.completeOperation(31L, 9L, "CONFIRM_DISPATCH", key)).thenReturn(1);
|
||||
}
|
||||
|
||||
private static List<OutboundDispatchReadMapper.Binding> bindings() {
|
||||
return List.of(binding(11L, "I-11"), binding(12L, "I-12"));
|
||||
}
|
||||
|
||||
+2
@@ -48,6 +48,8 @@ class OutboundDispatchSqlContractTest {
|
||||
.contains("outbound-dispatches/{containerBookingId}/reverse:")
|
||||
.contains("every real inbound binding", "No partial success is possible")
|
||||
.contains("Once an actual DEPARTED_WAREHOUSE node exists")
|
||||
.contains("absence never blocks manual outbound confirmation")
|
||||
.contains("PENDING or any non-terminal state never affects gate.passed")
|
||||
.contains("expectedVersion")
|
||||
.contains("Idempotency-Key")
|
||||
.doesNotContain("confirm-dispatch", "reverse-dispatch", "check-gate", "inboundId, containerBookingId");
|
||||
|
||||
Reference in New Issue
Block a user