fix(firstleg): preserve confirmed loading history in departure gates
This commit is contained in:
+13
-6
@@ -163,13 +163,13 @@ public class OutboundDispatchService {
|
|||||||
List<InboundBinding> inboundDtos = new ArrayList<>();
|
List<InboundBinding> inboundDtos = new ArrayList<>();
|
||||||
List<OutboundDispatchEntity> states = new ArrayList<>();
|
List<OutboundDispatchEntity> states = new ArrayList<>();
|
||||||
for (OutboundDispatchReadMapper.Binding binding : bindings) {
|
for (OutboundDispatchReadMapper.Binding binding : bindings) {
|
||||||
GateResult gate = gate(binding, pick, ocean, evidence, containerContributorGates);
|
OutboundDispatchEntity state = repository.dispatch(binding.tenantId(), binding.inboundId(),
|
||||||
|
binding.containerBookingId());
|
||||||
|
GateResult gate = gate(binding, state, pick, ocean, evidence, containerContributorGates);
|
||||||
inboundGates.add(new InboundGate(binding.inboundId(), binding.inboundCode(), gate));
|
inboundGates.add(new InboundGate(binding.inboundId(), binding.inboundCode(), gate));
|
||||||
inboundDtos.add(new InboundBinding(binding.orderId(), binding.orderNo(), binding.inboundId(),
|
inboundDtos.add(new InboundBinding(binding.orderId(), binding.orderNo(), binding.inboundId(),
|
||||||
binding.inboundCode(), binding.tenantId(), binding.customerId(), binding.customerName(),
|
binding.inboundCode(), binding.tenantId(), binding.customerId(), binding.customerName(),
|
||||||
binding.inboundStatus(), id(pick), status(pick)));
|
binding.inboundStatus(), id(pick), status(pick)));
|
||||||
OutboundDispatchEntity state = repository.dispatch(binding.tenantId(), binding.inboundId(),
|
|
||||||
binding.containerBookingId());
|
|
||||||
if (state != null) states.add(state);
|
if (state != null) states.add(state);
|
||||||
}
|
}
|
||||||
StateView visible = visibleStateForRead(states, bindings.size());
|
StateView visible = visibleStateForRead(states, bindings.size());
|
||||||
@@ -180,14 +180,21 @@ public class OutboundDispatchService {
|
|||||||
visible.status(), visible.version(), List.copyOf(inboundDtos), containerGate);
|
visible.status(), visible.version(), List.copyOf(inboundDtos), containerGate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private GateResult gate(OutboundDispatchReadMapper.Binding binding, OutboundDispatchReadMapper.IdStatus pick,
|
private GateResult gate(OutboundDispatchReadMapper.Binding binding, OutboundDispatchEntity dispatch,
|
||||||
|
OutboundDispatchReadMapper.IdStatus pick,
|
||||||
OutboundDispatchReadMapper.OceanBinding ocean, OutboundDispatchReadMapper.Evidence evidence,
|
OutboundDispatchReadMapper.OceanBinding ocean, OutboundDispatchReadMapper.Evidence evidence,
|
||||||
List<GateItem> containerContributorGates) {
|
List<GateItem> containerContributorGates) {
|
||||||
List<GateItem> items = new ArrayList<>();
|
List<GateItem> items = new ArrayList<>();
|
||||||
items.add(item("INBOUND_NOT_HELD", "入库单未扣货", !Boolean.TRUE.equals(binding.holdFlag()),
|
items.add(item("INBOUND_NOT_HELD", "入库单未扣货", !Boolean.TRUE.equals(binding.holdFlag()),
|
||||||
Boolean.TRUE.equals(binding.holdFlag()) ? "HELD" : "RELEASED", "RELEASED"));
|
Boolean.TRUE.equals(binding.holdFlag()) ? "HELD" : "RELEASED", "RELEASED"));
|
||||||
items.add(item("INBOUND_LOADED", "入库单装柜完成", "LOADED".equals(binding.inboundStatus()),
|
// Later delivery progress does not erase this binding's confirmed departure.
|
||||||
binding.inboundStatus(), "LOADED"));
|
// Never infer loading completion from a later inbound status alone.
|
||||||
|
boolean confirmedLoadedHistory = dispatch != null && dispatch.getConfirmedAt() != null
|
||||||
|
&& OutboundDispatchStatus.WAITING_FOR_LOADING.name().equals(dispatch.getStatus())
|
||||||
|
&& ("DELIVERED".equals(binding.inboundStatus()) || "COMPLETED".equals(binding.inboundStatus()));
|
||||||
|
items.add(item("INBOUND_LOADED", "入库单装柜完成",
|
||||||
|
"LOADED".equals(binding.inboundStatus()) || confirmedLoadedHistory,
|
||||||
|
binding.inboundStatus(), confirmedLoadedHistory ? "已确认离仓,装柜历史已保留" : "LOADED"));
|
||||||
items.add(item("LOADING_PLAN_COMPLETED", "柜次装柜计划完成", "COMPLETED".equals(binding.loadingPlanStatus()),
|
items.add(item("LOADING_PLAN_COMPLETED", "柜次装柜计划完成", "COMPLETED".equals(binding.loadingPlanStatus()),
|
||||||
binding.loadingPlanStatus(), "COMPLETED"));
|
binding.loadingPlanStatus(), "COMPLETED"));
|
||||||
items.add(item("PDA_PICK_COMPLETED", "PDA柜次拣货完成", pick != null && "COMPLETED".equals(pick.status()),
|
items.add(item("PDA_PICK_COMPLETED", "PDA柜次拣货完成", pick != null && "COMPLETED".equals(pick.status()),
|
||||||
|
|||||||
+69
@@ -11,6 +11,9 @@ import com.baiyue.firstleg.outbound.dispatch.service.OutboundDispatchService;
|
|||||||
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
|
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
@@ -31,6 +34,72 @@ class OutboundDispatchServiceTest {
|
|||||||
when(repository.timeline(any(), any(), any())).thenReturn(List.of());
|
when(repository.timeline(any(), any(), any())).thenReturn(List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = {"DELIVERED", "COMPLETED"})
|
||||||
|
void confirmedDepartureKeepsLoadedGateWhenInboundAdvances(String inboundStatus) {
|
||||||
|
stubContainer(List.of(bindingWithStatus(11L, inboundStatus)));
|
||||||
|
OutboundDispatchEntity confirmed = state(31L, 11L, "WAITING_FOR_LOADING", 1);
|
||||||
|
confirmed.setConfirmedAt(LocalDateTime.of(2026, 10, 1, 12, 0));
|
||||||
|
when(repository.dispatch(7L, 11L, 22L)).thenReturn(confirmed);
|
||||||
|
Detail detail = serviceWithoutCarrier().detail(22L);
|
||||||
|
assertThat(detail.aggregate().gate().passed()).isTrue();
|
||||||
|
assertThat(detail.aggregate().inbounds().get(0).inboundStatus()).isEqualTo(inboundStatus);
|
||||||
|
assertThat(detail.aggregate().gate().inbounds().get(0).gate().items())
|
||||||
|
.filteredOn(item -> item.code().equals("INBOUND_LOADED"))
|
||||||
|
.singleElement().satisfies(item -> {
|
||||||
|
assertThat(item.passed()).isTrue();
|
||||||
|
assertThat(item.actual()).isEqualTo(inboundStatus);
|
||||||
|
assertThat(item.requirement()).contains("已确认离仓");
|
||||||
|
});
|
||||||
|
verify(repository, never()).confirm(anyLong(), anyInt(), anyLong(), anyString(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = {"DELIVERED", "COMPLETED"})
|
||||||
|
void laterInboundStateWithoutConfirmedDepartureCannotPassOrConfirm(String inboundStatus) {
|
||||||
|
var bindings = List.of(bindingWithStatus(11L, inboundStatus));
|
||||||
|
stubContainer(bindings);
|
||||||
|
when(repository.lockBindings(22L)).thenReturn(bindings);
|
||||||
|
when(repository.claim(eq(9L), eq("CONFIRM_DISPATCH"), eq("later-unconfirmed"), anyString())).thenReturn(true);
|
||||||
|
var service = serviceWithoutCarrier();
|
||||||
|
assertThat(service.checkGate(22L).passed()).isFalse();
|
||||||
|
assertThatThrownBy(() -> service.confirm(22L, 9L, "admin", "later-unconfirmed", new ActionRequest(0, null)))
|
||||||
|
.isInstanceOf(BizException.class).hasMessageContaining("门禁");
|
||||||
|
verify(repository, never()).confirm(anyLong(), anyInt(), anyLong(), anyString(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reversedDepartureAndMissingConfirmationTimeDoNotProveLoadedHistory() {
|
||||||
|
stubContainer(List.of(bindingWithStatus(11L, "DELIVERED")));
|
||||||
|
var state = state(31L, 11L, "READY_FOR_DISPATCH", 2);
|
||||||
|
state.setConfirmedAt(LocalDateTime.of(2026, 10, 1, 12, 0));
|
||||||
|
when(repository.dispatch(7L, 11L, 22L)).thenReturn(state);
|
||||||
|
assertThat(serviceWithoutCarrier().checkGate(22L).passed()).isFalse();
|
||||||
|
state.setStatus("WAITING_FOR_LOADING");
|
||||||
|
state.setConfirmedAt(null);
|
||||||
|
assertThat(serviceWithoutCarrier().checkGate(22L).passed()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void confirmedDepartureDoesNotBypassLoadingOrOtherAuthoritativeGates() {
|
||||||
|
var confirmed = state(31L, 11L, "WAITING_FOR_LOADING", 1);
|
||||||
|
confirmed.setConfirmedAt(LocalDateTime.of(2026, 10, 1, 12, 0));
|
||||||
|
stubContainer(List.of(bindingWithStatus(11L, "LOADING")));
|
||||||
|
when(repository.dispatch(7L, 11L, 22L)).thenReturn(confirmed);
|
||||||
|
assertThat(serviceWithoutCarrier().checkGate(22L).passed()).isFalse();
|
||||||
|
stubContainer(List.of(bindingWithStatus(11L, "DELIVERED")));
|
||||||
|
when(repository.evidence(22L, 44L)).thenReturn(new OutboundDispatchReadMapper.Evidence(4, 1));
|
||||||
|
var gate = serviceWithoutCarrier().checkGate(22L);
|
||||||
|
assertThat(gate.passed()).isFalse();
|
||||||
|
assertThat(gate.inbounds().get(0).gate().items()).filteredOn(item -> item.code().equals("LOADING_EVIDENCE_COMPLETE"))
|
||||||
|
.extracting(GateItem::passed).containsExactly(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OutboundDispatchReadMapper.Binding bindingWithStatus(long inboundId, String status) {
|
||||||
|
return new OutboundDispatchReadMapper.Binding(10L, "O-10", inboundId, "I-" + inboundId, 7L, 8L, "客户A",
|
||||||
|
status, false, 22L, "CAB-22", "C-22", 44L, "COMPLETED");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void containerEvidenceFailureFailsEveryBindingConsistently() {
|
void containerEvidenceFailureFailsEveryBindingConsistently() {
|
||||||
OutboundDispatchService service = serviceWithoutCarrier();
|
OutboundDispatchService service = serviceWithoutCarrier();
|
||||||
|
|||||||
Reference in New Issue
Block a user