fix(lastleg): repair dispatch cancellation and return execution contracts
This commit is contained in:
+9
@@ -24,6 +24,15 @@ import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class DeliveryFeeCoverageServiceTest {
|
||||
/** 同一排车明细中的外接货覆盖不能免掉普通来源的应收。 */
|
||||
@Test void externalFreightCoversOnlyItsOwnInboundEvenWhenAnItemIsShared() {
|
||||
when(finance.sources(anyString(), any())).thenReturn(ApiResponse.success(List.of(fee(101, 11))));
|
||||
var preview = service.preview("DP-1");
|
||||
var selection = new DeliveryFeeCoverageModels.Selection(711L, 1, fee(101, 11).sourceHash(), List.of("LOADING_FEE"));
|
||||
var result = service.confirm("DP-1", coverageRequest(preview.cargoHash(), List.of(selection)), "OP", Set.of("IN-A"));
|
||||
assertThat(result.includedServices()).doesNotContain("DELIVERY_FEE");
|
||||
assertThat(result.uncovered("DELIVERY_FEE")).extracting(DeliveryFeeCoverageModels.Cargo::inboundCode).containsExactly("IN-B");
|
||||
}
|
||||
final DispatchOrderMapper dispatches = mock(DispatchOrderMapper.class);
|
||||
final DispatchItemMapper items = mock(DispatchItemMapper.class);
|
||||
final DispatchFeeCargoMapper cargo = mock(DispatchFeeCargoMapper.class);
|
||||
|
||||
+30
-2
@@ -12,6 +12,34 @@ import java.util.UUID;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class DispatchFeeCargoMapperTest {
|
||||
/** 原退回实收只重派其已冻结货量,不借同托其他货物放大来源。 */
|
||||
@Test void returnedReceiptRetainsItsExactSourceInsteadOfBecomingAWholePallet() {
|
||||
var dispatch = new com.baiyue.lastleg.dispatch.entity.DispatchOrder(); dispatch.setId(1L); dispatch.setCustomerId("101");
|
||||
var item = new com.baiyue.lastleg.dispatch.entity.DispatchItem(); item.setId(201L); item.setWarehousePalletId(501L);
|
||||
item.setReturnReceiptId(701L); item.setSourceStage("RECEIVED"); item.setInboundCode("IN-A");
|
||||
item.setPackageCount(2); item.setDestinationType("FBA"); item.setDestinationWarehouseCode("ONT8");
|
||||
var query = new com.baiyue.lastleg.delivery.service.DispatchCargoSourceQuery(
|
||||
new com.baiyue.lastleg.delivery.repository.DispatchCargoSourceRepository(mapper));
|
||||
assertThat(query.sources(dispatch, java.util.List.of(item))).singleElement().satisfies(source -> {
|
||||
assertThat(source.inboundCode()).isEqualTo("IN-A"); assertThat(source.packages()).isEqualTo(2);
|
||||
});
|
||||
}
|
||||
/** 展示普通货或外接货均不能改变真实组成;保留同客户、同仓及本次拣货边界。 */
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings={"IN-A", "IN-B"})
|
||||
void actualSourceQueryDoesNotDependOnWhichInboundWasChosenForDisplay(String displayInbound) {
|
||||
jdbc.update("UPDATE ll_dispatch_item SET inbound_code=?,plan_pallet_id=801", displayInbound);
|
||||
var dispatch = new com.baiyue.lastleg.dispatch.entity.DispatchOrder(); dispatch.setId(1L); dispatch.setCustomerId("101");
|
||||
var item = new com.baiyue.lastleg.dispatch.entity.DispatchItem(); item.setId(201L); item.setWarehousePalletId(501L);
|
||||
item.setInboundCode(displayInbound); item.setPackageCount(20); item.setDestinationType("FBA"); item.setDestinationWarehouseCode("ONT8");
|
||||
var query = new com.baiyue.lastleg.delivery.service.DispatchCargoSourceQuery(
|
||||
new com.baiyue.lastleg.delivery.repository.DispatchCargoSourceRepository(mapper));
|
||||
var sources = query.sources(dispatch, java.util.List.of(item));
|
||||
assertThat(sources).extracting(com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.SourceCargo::inboundCode).containsExactly("IN-A", "IN-B");
|
||||
assertThat(sources).extracting(com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.SourceCargo::packages).containsExactly(10, 10);
|
||||
item.setPackageCount(19);
|
||||
assertThatThrownBy(() -> query.sources(dispatch, java.util.List.of(item))).hasMessageContaining("箱数不一致");
|
||||
}
|
||||
JdbcTemplate jdbc; DispatchFeeCargoMapper mapper;
|
||||
@BeforeEach void setup() throws Exception {
|
||||
var dataSource = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
@@ -41,10 +69,10 @@ class DispatchFeeCargoMapperTest {
|
||||
assertThat(rows).extracting(row -> row.packages()).containsExactly(10L, 10L);
|
||||
assertThat(mapper.list("202", 1)).isEmpty();
|
||||
}
|
||||
@Test void aSelectedPlanPalletCannotBorrowOtherInboundStockOnTheSamePhysicalPallet() {
|
||||
@Test void aPhysicalSharedPalletIncludesEveryActualSourceRegardlessOfItsDisplayPlan() {
|
||||
jdbc.update("UPDATE ll_dispatch_item SET plan_pallet_id=801");
|
||||
jdbc.update("INSERT INTO ll_inbound_plan_pallet_allocation VALUES(801,901)");
|
||||
jdbc.update("INSERT INTO ll_inbound_plan_line VALUES(901,'101',401)");
|
||||
assertThat(mapper.list("101", 1)).extracting(row -> row.sourceInboundId()).containsExactly("11");
|
||||
assertThat(mapper.list("101", 1)).extracting(row -> row.sourceInboundId()).containsExactly("11", "12");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ class DeliveryStatusPolicyTest {
|
||||
@Test
|
||||
void exposesOnlyServerAuthorizedActionsForEachState() {
|
||||
assertThat(DeliveryStatusPolicy.allowedActions("WAIT_DELIVERY"))
|
||||
.containsExactly("START_DELIVERY", "REJECT_DELIVERY", "CANCEL_DELIVERY");
|
||||
.containsExactly("START_DELIVERY", "REJECT_DELIVERY");
|
||||
assertThat(DeliveryStatusPolicy.allowedActions("DELIVERING"))
|
||||
.containsExactly("REJECT_DELIVERY", "MARK_RETURN");
|
||||
assertThat(DeliveryStatusPolicy.allowedActions("DELIVERED")).containsExactly("MARK_RETURN");
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.baiyue.lastleg.delivery.service;
|
||||
|
||||
import com.baiyue.lastleg.delivery.entity.*;
|
||||
import com.baiyue.lastleg.delivery.mapper.*;
|
||||
import com.baiyue.lastleg.delivery.repository.DeliveryExecutionRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class DeliveryExecutionGuardTest {
|
||||
/** 直接消费真实仓储访问适配器,状态和数量由生产门禁判定。 */
|
||||
@Test void departureRequiresCompleteExecutableDeliveriesAndLocksThem() {
|
||||
var deliveries = mock(DeliveryOrderMapper.class);
|
||||
var guard = new DeliveryExecutionGuard(new DeliveryExecutionRepository(mock(OutboundOrderMapper.class), deliveries));
|
||||
var outbound = new OutboundOrder(); outbound.setId(1L); outbound.setCustomerId("C");
|
||||
outbound.setOutboundNo("OUT-1"); outbound.setPackageCount(2);
|
||||
var delivery = new DeliveryOrder(); delivery.setCustomerId("C"); delivery.setOutboundOrderId(1L);
|
||||
delivery.setOutboundNo("OUT-1"); delivery.setStatus("WAIT_DELIVERY"); delivery.setPackageCount(2);
|
||||
when(deliveries.lockByOutbound("C", 1L)).thenReturn(List.of(delivery));
|
||||
guard.requireDeparture(outbound);
|
||||
delivery.setStatus("CANCELLED");
|
||||
assertThatThrownBy(() -> guard.requireDeparture(outbound)).hasMessageContaining("已取消");
|
||||
delivery.setStatus("WAIT_DELIVERY"); delivery.setPackageCount(1);
|
||||
assertThatThrownBy(() -> guard.requireDeparture(outbound)).hasMessageContaining("完整覆盖");
|
||||
delivery.setPackageCount(2); delivery.setCustomerId("OTHER");
|
||||
assertThatThrownBy(() -> guard.requireDeparture(outbound)).hasMessageContaining("来源不一致");
|
||||
when(deliveries.lockByOutbound("C", 1L)).thenReturn(List.of());
|
||||
assertThatThrownBy(() -> guard.requireDeparture(outbound)).hasMessageContaining("缺少对应派送单");
|
||||
verify(deliveries, times(5)).lockByOutbound("C", 1L);
|
||||
}
|
||||
}
|
||||
+1
@@ -26,6 +26,7 @@ class DeliveryReturnEvidencePersistenceTest {
|
||||
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.execute("CREATE TABLE ll_return_carton_stock_receipt(id BIGINT,customer_id VARCHAR(20),source_dispatch_item_id BIGINT,source_outbound_pallet_item_id BIGINT,packages BIGINT,received_at TIMESTAMP,delivery_order_id BIGINT,receipt_no VARCHAR(64))");
|
||||
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)");
|
||||
|
||||
+10
@@ -134,6 +134,16 @@ class DeliveryTransferServiceTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new DeliveryTransferService();
|
||||
ReflectionTestUtils.setField(service, "deliveryExecution", new DeliveryExecutionGuard(new com.baiyue.lastleg.delivery.repository.DeliveryExecutionRepository(outboundOrders, deliveryOrders)));
|
||||
var actualSources = mock(DispatchCargoSourceQuery.class);
|
||||
ReflectionTestUtils.setField(service, "cargoSources", actualSources);
|
||||
org.mockito.Mockito.lenient().when(actualSources.sources(any(), any())).thenAnswer(call -> {
|
||||
List<DispatchItem> selected = call.getArgument(1);
|
||||
return selected.stream().map(item -> new com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.SourceCargo(
|
||||
item.getInboundCode(), item.getWarehousePalletId() == null ? null : item.getWarehousePalletId().toString(), item.getPalletNo(),
|
||||
java.util.Objects.requireNonNullElse(item.getPackageCount(), 0), com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.destinationKey(
|
||||
item.getDestinationType(), item.getDestinationWarehouseCode(), item.getFbaWarehouseCode(), item.getDeliveryAddress(), item.getPostalCode()))).toList();
|
||||
});
|
||||
ReflectionTestUtils.setField(service, "fleetBindings", fleetBindings);
|
||||
ReflectionTestUtils.setField(service, "bookingFreight", bookingFreight);
|
||||
ReflectionTestUtils.setField(service, "feeCoverage", feeCoverage);
|
||||
|
||||
Reference in New Issue
Block a user