feat(fleet): bind quote versions and reconcile actual return quantities

This commit is contained in:
2026-10-03 13:32:55 +08:00
parent cb6c54ffe5
commit a6fd0ba3c5
60 changed files with 1363 additions and 75 deletions
@@ -32,9 +32,9 @@ class FleetHandoverPdaQueryTest {
""");
when(history.selectList(any())).thenReturn(List.of(departed));
when(dispatches.transitEvidence("PC-A", false)).thenReturn(new FleetTransitEvidence("PC-A", "CUSTOMER-A", "92", 30, List.of(
new FleetTransitEvidence.Delivery(new FleetHandoverPolicy.Delivery("CUSTOMER-A", "PC-A", "DL-DONE", "FBA:ONE", 10, Instant.parse("2026-10-03T01:00:00Z")),
new FleetTransitEvidence.Delivery(new FleetHandoverPolicy.Delivery("CUSTOMER-A", "PC-A", "DL-DONE", "FBA:ONE", 10, Instant.parse("2026-10-03T01:00:00Z"), List.of()),
"DELIVERED", 2, new FleetTransitEvidence.Transport("ORIGINAL", "原司机", null)),
new FleetTransitEvidence.Delivery(new FleetHandoverPolicy.Delivery("CUSTOMER-A", "PC-A", "DL-PENDING", "FBA:TWO", 20, null),
new FleetTransitEvidence.Delivery(new FleetHandoverPolicy.Delivery("CUSTOMER-A", "PC-A", "DL-PENDING", "FBA:TWO", 20, null, List.of()),
"WAIT_DELIVERY", 3, new FleetTransitEvidence.Transport("REPLACEMENT", "接替司机", null)))));
}
@@ -41,14 +41,14 @@ class FleetHandoverPolicyTest {
}
@Test void sameDestinationCannotMixCustomerOrDispatchFacts() {
var wrong = new Delivery("A", "PC-B", "D-B", "FBA:ONE", 20, null);
var wrong = new Delivery("A", "PC-B", "D-B", "FBA:ONE", 20, null, List.of());
assertThrows(IllegalArgumentException.class, () -> inspectBoth(delivery(a, null), wrong));
}
@Test void missingDuplicateAndChangedQuantityFactsAreRejected() {
assertThrows(IllegalArgumentException.class, () -> inspectBoth(delivery(a, null)));
assertThrows(IllegalArgumentException.class, () -> inspectBoth(delivery(a, null), delivery(a, null), delivery(b, null)));
var wrong = new Delivery("B", "PC-B", "D-B", "FBA:ONE", 19, null);
var wrong = new Delivery("B", "PC-B", "D-B", "FBA:ONE", 19, null, List.of());
assertThrows(IllegalArgumentException.class, () -> inspectBoth(delivery(a, null), wrong));
}
@@ -90,7 +90,7 @@ class FleetHandoverPolicyTest {
}
private static Delivery delivery(Cargo cargo, Instant terminalAt) {
return new Delivery(cargo.customerId(), cargo.dispatchNo(), "D-" + cargo.customerId(),
cargo.destinationKey(), cargo.packages(), terminalAt);
cargo.destinationKey(), cargo.packages(), terminalAt, List.of());
}
private static Load load(int pallets, int kg, int cbm) {
return new Load(pallets, BigDecimal.valueOf(kg), BigDecimal.valueOf(cbm));
@@ -70,7 +70,7 @@ class FleetHandoverTransactionTest {
true, "APPROVED", "file", 0, "1", LocalDateTime.now(), qualification, "VALID", null, null));
var inspection = mock(FleetHandoverInspectionService.class);
var cargo = new FleetHandoverPolicy.Cargo("CUSTOMER-A", "PC-A", "item", "pallet", "FBA:ONT8", 10);
var delivery = new FleetHandoverPolicy.Delivery("CUSTOMER-A", "PC-A", "DL-A", "FBA:ONT8", 10, null);
var delivery = new FleetHandoverPolicy.Delivery("CUSTOMER-A", "PC-A", "DL-A", "FBA:ONT8", 10, null, List.of());
var evidence = new FleetTransitEvidence("PC-A", "CUSTOMER-A", "92", 10,
List.of(new FleetTransitEvidence.Delivery(delivery, "WAIT_DELIVERY", 1, new FleetTransitEvidence.Transport("PLATE-11", "司机", "phone"))));
when(inspection.prepare(anyString(), any(), any(), eq(true))).thenAnswer(call -> new FleetHandoverInspectionService.Prepared(state().getRecordVersion(),
@@ -63,10 +63,10 @@ class FleetTransitEvidenceTest {
@Test void priorSignatureRemainsTerminalAfterLaterReturnRequest() {
var signed = LocalDateTime.of(2026, 10, 3, 9, 0);
delivery.setStatus("RETURN_PENDING"); delivery.setSignedAt(signed);
delivery.setStatus("RETURN_PENDING"); delivery.setSignedAt(signed); delivery.setSignedPackages(10);
var fact = adapter.transitEvidence("PC-1", false);
assertEquals(signed.atZone(ZoneId.systemDefault()).toInstant(), fact.deliveries().get(0).cargo().terminalAt());
verifyNoInteractions(operations, workflow);
verify(operations).returnReceipts("CUSTOMER-A", 41L); verifyNoInteractions(workflow);
}
@Test void unsignedReturnRequiresImmutableReturnOperation() {
@@ -74,6 +74,7 @@ class FleetTransitEvidenceTest {
assertThrows(org.springframework.web.server.ResponseStatusException.class, () -> adapter.transitEvidence("PC-1", false));
var returned = LocalDateTime.of(2026, 10, 3, 10, 0);
when(operations.returnedAt("CUSTOMER-A", 41L)).thenReturn(returned);
when(operations.returnReceipts("CUSTOMER-A", 41L)).thenReturn(List.of(new com.baiyue.lastleg.delivery.model.DeliveryReturnReceiptFact(1L, 51L, 61L, 10, returned)));
assertEquals(returned.atZone(ZoneId.systemDefault()).toInstant(),
adapter.transitEvidence("PC-1", false).deliveries().get(0).cargo().terminalAt());
}
@@ -104,7 +105,7 @@ class FleetTransitEvidenceTest {
}
@Test void signedCargoStaysOnOriginalAssignmentAndStaleVersionCannotOverwrite() {
delivery.setStatus("DELIVERED"); delivery.setSignedAt(LocalDateTime.of(2026, 10, 3, 9, 0));
delivery.setStatus("DELIVERED"); delivery.setSignedAt(LocalDateTime.of(2026, 10, 3, 9, 0)); delivery.setSignedPackages(10);
var evidence = adapter.transitEvidence("PC-1", true);
var assignment = new com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.Assignment("NEW-PLATE", "接替司机", "phone");
adapter.handover(evidence, assignment);
@@ -113,4 +114,37 @@ class FleetTransitEvidenceTest {
assertThrows(org.springframework.web.server.ResponseStatusException.class, () -> adapter.handover(evidence, assignment));
verifyNoInteractions(workflow);
}
@Test void partialReturnThenRemainingHandoverThenSignatureConservesTheOriginalShipment() {
var returnedAt = LocalDateTime.of(2026, 10, 3, 9, 0);
delivery.setStatus("RETURN_PENDING");
when(operations.returnReceipts("CUSTOMER-A", 41L)).thenReturn(List.of(new com.baiyue.lastleg.delivery.model.DeliveryReturnReceiptFact(1L, 51L, 61L, 4, returnedAt)));
var evidence = adapter.transitEvidence("PC-1", true);
var departed = returnedAt.minusHours(1).atZone(ZoneId.systemDefault()).toInstant();
var handedOver = returnedAt.plusMinutes(1).atZone(ZoneId.systemDefault()).toInstant();
var load = new com.baiyue.lastleg.fleet.domain.TripCapacityPolicy.Load(1, new java.math.BigDecimal("100"), java.math.BigDecimal.ONE);
var inspected = FleetHandoverPolicy.inspect(departed, handedOver, handedOver,
List.of(new FleetHandoverPolicy.Cargo("CUSTOMER-A", "PC-1", "51", "61", "FBA:ONT8", 10)),
List.of(evidence.deliveries().get(0).cargo()), List.of(new FleetHandoverPolicy.Pallet("61", load)), load);
assertEquals(6, inspected.remainingCargo().get(0).packages());
assertEquals(load, inspected.occupied()); // No invented proportional weight reduction.
assertNotNull(adapter.completion("PC-1", false).blockReason());
when(deliveries.lockByNo("CUSTOMER-A", "DL-1")).thenReturn(delivery);
when(deliveries.update(isNull(), any())).thenReturn(1);
adapter.handover(evidence, new com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.Assignment("NEW", "司机", "phone"));
delivery.setStatus("DELIVERED"); delivery.setSignedAt(returnedAt.plusMinutes(2)); delivery.setSignedPackages(6);
var complete = adapter.completion("PC-1", false);
assertNull(complete.blockReason());
var outcome = complete.outcomes().get(0);
assertEquals(10, outcome.packages()); assertEquals(6, outcome.signedPackages()); assertEquals(4, outcome.returnedPackages());
assertEquals(0, outcome.remainingPackages());
// A later return of signed goods cannot rewrite the original 6 signed + 4 returned journey.
when(operations.returnReceipts("CUSTOMER-A", 41L)).thenReturn(List.of(
new com.baiyue.lastleg.delivery.model.DeliveryReturnReceiptFact(1L, 51L, 61L, 4, returnedAt),
new com.baiyue.lastleg.delivery.model.DeliveryReturnReceiptFact(2L, 51L, 61L, 2, returnedAt.plusMinutes(3))));
delivery.setStatus("RETURN_PENDING");
assertEquals(outcome, adapter.completion("PC-1", false).outcomes().get(0));
assertEquals(10, delivery.getPackageCount());
verifyNoInteractions(workflow);
}
}