feat(lastleg): redispatch returned cargo and reconcile source signatures

This commit is contained in:
2026-10-03 16:06:54 +08:00
parent e740047e03
commit 8700fc3423
39 changed files with 1605 additions and 139 deletions
@@ -0,0 +1,91 @@
package com.baiyue.lastleg.delivery;
import com.baiyue.lastleg.delivery.model.SourceDeliveryCompletionPolicy;
import com.baiyue.lastleg.delivery.model.SourceDeliveryCompletionPolicy.*;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
class SourceDeliveryCompletionPolicyTest {
private final LocalDateTime departure = LocalDateTime.of(2026, 10, 3, 1, 0);
private final LocalDateTime signature = departure.plusHours(2);
private final List<Pick> picks = List.of(new Pick(1,10,"CODE:A",101,4,1L,null,null,null), new Pick(2,10,"CODE:A",102,6,2L,null,null,null));
private Delivery delivery(long signed) { return new Delivery(20,10,"CODE:A",10,signed,signature,departure); }
private Result evaluate(long order, long original, List<Delivery> deliveries, List<Pick> facts, List<Returned> returns) {
return SourceDeliveryCompletionPolicy.evaluate(new Source("SOURCE-" + order, order, original), deliveries, facts, returns, true);
}
@Test void onlyReturnedSourceWaitsWhileOtherSourceIsProvenFullySigned() {
var returns = List.of(new Returned(30,20,101,2,departure.plusHours(1)));
assertThat(evaluate(101,4,List.of(delivery(8)),picks,returns)).satisfies(r -> {
assertThat(r.provenSignedPackages()).isEqualTo(2); assertThat(r.fullySigned()).isFalse(); assertThat(r.unresolvedReasons()).isEmpty(); });
assertThat(evaluate(102,6,List.of(delivery(8)),picks,returns).fullySigned()).isTrue();
}
@Test void postSignatureReturnDoesNotEraseHistoricSignature() {
assertThat(evaluate(101,4,List.of(delivery(10)),picks,List.of(new Returned(30,20,101,2,signature.plusHours(1)))).fullySigned()).isTrue();
}
@Test void laterRedeliveryOfPreviouslyUnsignedBoxesCompletesSource() {
var deliveries = List.of(delivery(8), new Delivery(21,11,"CODE:A",2,2L,signature.plusDays(1),signature.plusHours(3)));
var allPicks = List.of(picks.get(0),picks.get(1),new Pick(3,11,"CODE:A",101,2,null,300L,30L,2L));
assertThat(evaluate(101,4,deliveries,allPicks,List.of(new Returned(30,20,101,2,departure.plusHours(1)))).fullySigned()).isTrue();
}
@Test void totalSignatureCannotBeProportionallyAssignedWhenReturnSourceIsMissing() {
var r = evaluate(102,6,List.of(delivery(8)),picks,List.of());
assertThat(r.fullySigned()).isFalse(); assertThat(r.unresolvedReasons()).contains("PRE_SIGNATURE_RETURN_UNRESOLVED:20");
}
@Test void equalTimestampRequiresReconciliation() {
assertThat(evaluate(102,6,List.of(delivery(8)),picks,List.of(new Returned(30,20,101,2,signature))).unresolvedReasons()).isNotEmpty();
}
@Test void duplicateLedgerAndIncompleteHistoryNeverProveFullSignature() {
var returned = new Returned(30,20,101,1,departure.plusHours(1));
assertThat(evaluate(102,6,List.of(delivery(8)),picks,List.of(returned,returned)).fullySigned()).isFalse();
assertThat(SourceDeliveryCompletionPolicy.evaluate(new Source("S",102,6),List.of(delivery(10)),picks,List.of(),false).fullySigned()).isFalse();
}
@Test void ambiguousDestinationOrMissingPickCoverageBlocksCompletion() {
assertThat(evaluate(102,6,List.of(delivery(10),new Delivery(21,10,"CODE:A",10,10L,signature,departure)),picks,List.of()).fullySigned()).isFalse();
assertThat(evaluate(102,6,List.of(delivery(10)),List.of(picks.get(1)),List.of()).fullySigned()).isFalse();
}
@Test void postSignatureRedeliveryCannotDoubleCountOriginalBoxes() {
assertThat(evaluate(101,4,List.of(delivery(10),new Delivery(21,11,"CODE:A",2,2L,signature.plusDays(1),signature.plusHours(3))),
List.of(picks.get(0),picks.get(1),new Pick(3,11,"CODE:A",101,2,null,300L,30L,2L)),List.of()).unresolvedReasons()).contains("RETURN_PICK_ORIGIN_UNRESOLVED:3");
}
@Test void postSignatureReturnRedeliveryCannotFillTheNeverShippedOriginalRemainder() {
var deliveries=List.of(new Delivery(20,10,"CODE:A",6,6L,signature,departure),
new Delivery(21,11,"CODE:A",4,4L,signature.plusDays(1),signature.plusHours(3)));
var facts=List.of(new Pick(1,10,"CODE:A",101,6,1L,null,null,null),
new Pick(2,11,"CODE:A",101,4,null,300L,30L,4L));
var result=evaluate(101,10,deliveries,facts,List.of(new Returned(30,20,101,4,signature.plusHours(1))));
assertThat(result.provenSignedPackages()).isEqualTo(6);
assertThat(result.fullySigned()).isFalse(); assertThat(result.unresolvedReasons()).isEmpty();
}
@Test void missingReceiptOriginCannotSilentlyBecomeFreshCargo() {
var facts=List.of(new Pick(1,10,"CODE:A",101,4,null,null,null,null),picks.get(1));
assertThat(evaluate(101,4,List.of(delivery(10)),facts,List.of()).unresolvedReasons()).contains("PICK_RECEIPT_ORIGIN_UNRESOLVED");
}
@Test void unrelatedDestinationMissingEvidenceDoesNotBlockProvenSource() {
var result=evaluate(101,4,List.of(new Delivery(20,10,"CODE:A",4,4L,signature,departure),
new Delivery(21,10,"CODE:B",6,null,null,departure)),List.of(picks.get(0)),List.of());
assertThat(result.fullySigned()).isTrue();assertThat(result.unresolvedReasons()).isEmpty();
}
@Test void completeTwoHopPreSignatureReturnsContributeOnlyNewlySignedBoxes() {
var ds=List.of(new Delivery(20,10,"CODE:A",4,2L,departure.plusHours(2),departure),
new Delivery(21,11,"CODE:A",2,1L,departure.plusHours(5),departure.plusHours(3)),
new Delivery(22,12,"CODE:A",1,1L,departure.plusHours(8),departure.plusHours(6)));
var ps=List.of(picks.get(0),new Pick(3,11,"CODE:A",101,2,null,300L,30L,2L),
new Pick(4,12,"CODE:A",101,1,null,400L,40L,1L));
var rs=List.of(new Returned(30,20,101,2,departure.plusHours(1)),new Returned(40,21,101,1,departure.plusHours(4)));
var result=evaluate(101,4,ds,ps,rs);
assertThat(result.fullySigned()).isTrue();assertThat(result.provenSignedPackages()).isEqualTo(4);
}
@Test void repeatedReturnOfAlreadySignedCohortRemainsNonContributing() {
var ds=List.of(new Delivery(20,10,"CODE:A",6,6L,departure.plusHours(2),departure),
new Delivery(21,11,"CODE:A",4,2L,departure.plusHours(6),departure.plusHours(4)),
new Delivery(22,12,"CODE:A",2,2L,departure.plusHours(9),departure.plusHours(7)));
var ps=List.of(new Pick(1,10,"CODE:A",101,6,1L,null,null,null),new Pick(2,11,"CODE:A",101,4,null,300L,30L,4L),
new Pick(3,12,"CODE:A",101,2,null,400L,40L,2L));
var rs=List.of(new Returned(30,20,101,4,departure.plusHours(3)),new Returned(40,21,101,2,departure.plusHours(5)));
var result=evaluate(101,10,ds,ps,rs);
assertThat(result.fullySigned()).isFalse();assertThat(result.provenSignedPackages()).isEqualTo(6);
assertThat(result.unresolvedReasons()).isEmpty();
}
}