feat(fleet): bind quote versions and reconcile actual return quantities
This commit is contained in:
+34
@@ -0,0 +1,34 @@
|
||||
package com.baiyue.lastleg.delivery.model;
|
||||
import com.baiyue.lastleg.delivery.entity.DeliveryOrder;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class DeliveryCargoQuantitiesTest {
|
||||
private final LocalDateTime now = LocalDateTime.of(2026, 10, 3, 12, 0);
|
||||
@Test void unsignedQuantityRemainsOpenUntilAllCargoHasActualTerminalFacts() {
|
||||
var row = new DeliveryOrder(); row.setPackageCount(10);
|
||||
assertThat(DeliveryCargoQuantities.from(row, List.of(receipt(1, 4, now))))
|
||||
.isEqualTo(new DeliveryCargoQuantities(10, 0, 4, 6));
|
||||
row.setSignedAt(now.plusMinutes(1)); row.setSignedPackages(6);
|
||||
assertThat(DeliveryCargoQuantities.from(row, List.of(receipt(1, 4, now), receipt(2, 2, now.plusMinutes(2)))))
|
||||
.isEqualTo(new DeliveryCargoQuantities(10, 6, 4, 0));
|
||||
}
|
||||
@Test void missingSignatureQuantityCannotDefaultToTheWholeShipment() {
|
||||
var row = new DeliveryOrder(); row.setPackageCount(10); row.setSignedAt(now);
|
||||
assertThatThrownBy(() -> DeliveryCargoQuantities.from(row, List.of())).hasMessageContaining("签收数量");
|
||||
}
|
||||
@Test void duplicatedSourcesOrOverReturnCannotCloseCargo() {
|
||||
var row = new DeliveryOrder(); row.setPackageCount(10);
|
||||
assertThatThrownBy(() -> DeliveryCargoQuantities.from(row, List.of(receipt(1, 4, now), receipt(1, 4, now))))
|
||||
.hasMessageContaining("重复");
|
||||
assertThatThrownBy(() -> DeliveryCargoQuantities.from(row, List.of(receipt(1, 11, now)))).hasMessageContaining("超过");
|
||||
}
|
||||
@Test void laterReturnCannotFillAnUnprovenGapInAnEarlierSignature() {
|
||||
var row = new DeliveryOrder(); row.setPackageCount(10); row.setSignedAt(now); row.setSignedPackages(6);
|
||||
assertThatThrownBy(() -> DeliveryCargoQuantities.from(row, List.of(receipt(1, 4, now.plusMinutes(1)))))
|
||||
.hasMessageContaining("守恒");
|
||||
}
|
||||
private DeliveryReturnReceiptFact receipt(long id, long packages, LocalDateTime time) { return new DeliveryReturnReceiptFact(id, 2L, 3L, packages, time); }
|
||||
}
|
||||
+1
-1
@@ -13,7 +13,7 @@ class DeliveryStatusPolicyTest {
|
||||
.containsExactly("REJECT_DELIVERY", "MARK_RETURN");
|
||||
assertThat(DeliveryStatusPolicy.allowedActions("DELIVERED")).containsExactly("MARK_RETURN");
|
||||
assertThat(DeliveryStatusPolicy.isEditableTarget("DELIVERED")).isFalse();
|
||||
assertThat(DeliveryStatusPolicy.allowedActions("RETURN_PENDING")).isEmpty();
|
||||
assertThat(DeliveryStatusPolicy.allowedActions("RETURN_PENDING")).containsExactly("MARK_RETURN");
|
||||
assertThat(DeliveryStatusPolicy.allowedActions("CANCELLED")).isEmpty();
|
||||
}
|
||||
|
||||
|
||||
+14
@@ -31,6 +31,7 @@ class DeliveryCompletedOutboxServiceTest {
|
||||
ReflectionTestUtils.setField(service, "objectMapper", json);
|
||||
DeliveryOrder delivery = new DeliveryOrder();
|
||||
delivery.setId(1L); delivery.setDeliveryNo("DL-1"); delivery.setVersion(2L);
|
||||
delivery.setPackageCount(10); delivery.setSignedPackages(10);
|
||||
|
||||
service.publish(delivery, List.of(order(11, "101", "201", "IN-1", "ONT8"),
|
||||
order(12, "102", "202", "IN-2", "ACY2")), Instant.parse("2026-09-15T08:00:00Z"));
|
||||
@@ -100,6 +101,19 @@ class DeliveryCompletedOutboxServiceTest {
|
||||
verifyNoInteractions(mapper);
|
||||
}
|
||||
|
||||
@Test void unknownOrPartialSignatureCannotPublishAWholeDomesticInboundSignature() {
|
||||
var mapper = mock(LastlegEventOutboxMapper.class);
|
||||
var service = new DeliveryCompletedOutboxService(); ReflectionTestUtils.setField(service, "outbox", mapper);
|
||||
var delivery = new DeliveryOrder(); delivery.setPackageCount(10);
|
||||
var sources = List.of(order(11, "101", "201", "IN-1", "ONT8"));
|
||||
for (Integer invalid : java.util.Arrays.asList(null, 0, -1, 11)) {
|
||||
delivery.setSignedPackages(invalid);
|
||||
assertThat(service.publish(delivery, sources, Instant.now())).isEqualTo(DeliveryCompletedOutboxService.Publication.UNKNOWN_SIGNATURE_REQUIRES_RECONCILIATION);
|
||||
}
|
||||
delivery.setSignedPackages(6);
|
||||
assertThat(service.publish(delivery, sources, Instant.now())).isEqualTo(DeliveryCompletedOutboxService.Publication.MIXED_RETURN_REQUIRES_SOURCE_RECONCILIATION);
|
||||
verifyNoInteractions(mapper);
|
||||
}
|
||||
private LastlegOrder order(long id, String inboundId, String orderId, String inboundCode, String fba) {
|
||||
LastlegOrder value = new LastlegOrder();
|
||||
value.setId(id); value.setOrderNo("LM-" + id); value.setCustomerId("88");
|
||||
|
||||
+25
-1
@@ -48,7 +48,7 @@ class DeliveryDocumentServiceTest {
|
||||
ReflectionTestUtils.setField(service,"customerVisits",customerVisits);
|
||||
order = new DeliveryOrder(); order.setId(10L); order.setCustomerId("100");
|
||||
order.setDeliveryNo("DL-1"); order.setVersion(0L); order.setStatus("DELIVERED");
|
||||
order.setOutboundNo("OUT-1");
|
||||
order.setOutboundNo("OUT-1"); order.setPackageCount(10);
|
||||
}
|
||||
@Test void savedCategoryCountCountsCategoriesNotFilesAndSurvivesReload() {
|
||||
when(orders.lockByNo("100","DL-1")).thenReturn(order);
|
||||
@@ -215,6 +215,30 @@ class DeliveryDocumentServiceTest {
|
||||
verify(files,never()).download(anyString(),anyLong(),any(),anyLong());
|
||||
verifyNoInteractions(revisions,operations);
|
||||
}
|
||||
@Test void remainingSignatureFreezesActualQuantityAndPersistsDomesticWritebackBlocker() {
|
||||
order.setStatus("RETURN_PENDING");
|
||||
when(orders.lockByNo("100", "DL-1")).thenReturn(order);
|
||||
when(files.metadata("100", 10, RECEIPT, 13)).thenReturn(file(13));
|
||||
var outbound = new OutboundOrder(); outbound.setStatus("OUTBOUNDED");
|
||||
when(outboundOrders.findByNo("100", "OUT-1")).thenReturn(outbound);
|
||||
when(operations.returnReceipts("100", 10L)).thenReturn(List.of(new com.baiyue.lastleg.delivery.model.DeliveryReturnReceiptFact(1L, 2L, 3L, 4, LocalDateTime.now().minusMinutes(1))));
|
||||
when(deliveryCompletedOutbox.publish(eq(order), anyList(), any())).thenReturn(DeliveryCompletedOutboxService.Publication.MIXED_RETURN_REQUIRES_SOURCE_RECONCILIATION);
|
||||
service.save("100", "DL-1", "9", request(0, "residual-signature", List.of(new Binding(RECEIPT, List.of(13L)))));
|
||||
assertThat(order.getPackageCount()).isEqualTo(10); assertThat(order.getSignedPackages()).isEqualTo(6);
|
||||
assertThat(order.getSignedAt()).isNotNull();
|
||||
var captured = ArgumentCaptor.forClass(DeliveryOperation.class);
|
||||
verify(operations, times(3)).insert(captured.capture());
|
||||
assertThat(captured.getAllValues()).anyMatch(row -> "FIRSTLEG_COMPLETION_PENDING".equals(row.getOperationNode()) && row.getResult().contains("未发布"));
|
||||
}
|
||||
@Test void updatingDocumentsDuringLaterReturnNeverRewritesOriginalSignature() {
|
||||
order.setStatus("RETURN_PENDING"); order.setSignedAt(LocalDateTime.of(2026, 9, 1, 12, 0)); order.setSignedPackages(10);
|
||||
when(orders.lockByNo("100", "DL-1")).thenReturn(order);
|
||||
when(files.metadata("100", 10, RECEIPT, 13)).thenReturn(file(13));
|
||||
service.save("100", "DL-1", "9", request(0, "later-evidence", List.of(new Binding(RECEIPT, List.of(13L)))));
|
||||
assertThat(order.getSignedAt()).isEqualTo(LocalDateTime.of(2026, 9, 1, 12, 0));
|
||||
assertThat(order.getSignedPackages()).isEqualTo(10); assertThat(order.getStatus()).isEqualTo("RETURN_PENDING");
|
||||
verifyNoInteractions(deliveryCompletedOutbox, customerVisits);
|
||||
}
|
||||
private Save request(long version,String key,List<Binding> bindings) { return new Save(version,key,"BOL-1",bindings); }
|
||||
private File file(long id) { return new File(id,"a.pdf","application/pdf",1,LocalDateTime.of(2026,9,9,8,0)); }
|
||||
}
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.baiyue.lastleg.delivery.service;
|
||||
|
||||
import com.baiyue.lastleg.delivery.entity.DeliveryOrder;
|
||||
import com.baiyue.lastleg.delivery.mapper.DeliveryOperationMapper;
|
||||
import com.baiyue.lastleg.delivery.model.DeliveryCargoQuantities;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class DeliveryReturnEvidencePersistenceTest {
|
||||
@Test void twoItemsOnTheSamePalletResolveThroughTheOriginalPickPositionAndOrder() throws Exception {
|
||||
var data = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
var jdbc = new JdbcTemplate(data);
|
||||
jdbc.execute("CREATE TABLE ll_delivery_order(id BIGINT,customer_id VARCHAR(20),dispatch_order_id BIGINT,outbound_order_id BIGINT,outbound_no VARCHAR(20))");
|
||||
jdbc.execute("CREATE TABLE ll_exception_order(id BIGINT,customer_id VARCHAR(20),delivery_order_id BIGINT,exception_no VARCHAR(20),last_mile_order_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE ll_exception_return_item(exception_order_id BIGINT,customer_id VARCHAR(20),outbound_pallet_item_id BIGINT,inventory_position_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE ll_outbound_pallet_item(id BIGINT,customer_id VARCHAR(20),outbound_order_id BIGINT,warehouse_pallet_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE ll_dispatch_item(id BIGINT,customer_id VARCHAR(20),dispatch_order_id BIGINT,warehouse_pallet_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE ll_dispatch_pda_pick(id BIGINT,customer_id VARCHAR(20),task_id BIGINT,dispatch_item_id BIGINT,inventory_position_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE ll_dispatch_pda_pick_order(customer_id VARCHAR(20),pick_id BIGINT,order_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE ll_dispatch_pda_task(id BIGINT,customer_id VARCHAR(20),dispatch_order_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE ll_inventory_ledger(id BIGINT,customer_id VARCHAR(20),business_order_no VARCHAR(20),outbound_order_no VARCHAR(20),business_type VARCHAR(40),delta_packages BIGINT,source_event_id VARCHAR(100),occurred_at TIMESTAMP)");
|
||||
jdbc.update("INSERT INTO ll_delivery_order VALUES(1,'A',10,20,'OUT-1')");
|
||||
jdbc.update("INSERT INTO ll_exception_order VALUES(2,'A',1,'EX-1',71)");
|
||||
jdbc.update("INSERT INTO ll_exception_return_item VALUES(2,'A',21,41)");
|
||||
jdbc.update("INSERT INTO ll_outbound_pallet_item VALUES(21,'A',20,99)");
|
||||
jdbc.update("INSERT INTO ll_dispatch_item VALUES(31,'A',10,99),(32,'A',10,99)");
|
||||
jdbc.update("INSERT INTO ll_dispatch_pda_task VALUES(50,'A',10)");
|
||||
jdbc.update("INSERT INTO ll_dispatch_pda_pick VALUES(51,'A',50,31,41),(52,'A',50,32,42)");
|
||||
jdbc.update("INSERT INTO ll_dispatch_pda_pick_order VALUES('A',51,71),('A',52,72)");
|
||||
jdbc.update("INSERT INTO ll_inventory_ledger VALUES(60,'A','EX-1','OUT-1','RETURN_STOCK_IN',4,'RETURN_SOURCE:21:41',CURRENT_TIMESTAMP)");
|
||||
String sql = String.join(" ", DeliveryOperationMapper.class.getMethod("returnReceipts", String.class, Long.class)
|
||||
.getAnnotation(Select.class).value()).replace("#{customerId}", ":customerId").replace("#{deliveryOrderId}", ":deliveryOrderId");
|
||||
var named = new NamedParameterJdbcTemplate(data);
|
||||
var facts = named.query(sql, Map.of("customerId", "A", "deliveryOrderId", 1L), (row, index) ->
|
||||
new com.baiyue.lastleg.delivery.model.DeliveryReturnReceiptFact(row.getLong("ledger_id"), row.getLong("dispatch_item_id"), row.getLong("pallet_id"), row.getLong("packages"), row.getObject("received_at", LocalDateTime.class)));
|
||||
assertThat(facts).hasSize(1); assertThat(facts.get(0).dispatchItemId()).isEqualTo(31);
|
||||
assertThat(facts.get(0).packages()).isEqualTo(4);
|
||||
var delivery = new DeliveryOrder(); delivery.setPackageCount(10);
|
||||
assertThat(DeliveryCargoQuantities.from(delivery, facts).remaining()).isEqualTo(6);
|
||||
assertThat(named.queryForList(sql, Map.of("customerId", "OTHER", "deliveryOrderId", 1L))).isEmpty();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user