fix(lastleg): preserve return relabel delivery metadata and measurements
This commit is contained in:
+12
@@ -30,6 +30,18 @@ class DeliverySchemaContractTest {
|
||||
}
|
||||
}
|
||||
@Test
|
||||
void directOutboundSnapshotsKeepUnknownVolumeNullableAcrossBothBaselines() throws Exception {
|
||||
for (Path path : java.util.List.of(Path.of("src/main/resources/db/lastleg.sql"), Path.of("../deploy/sql/海外区域统一增量-MySQL8.sql"))) {
|
||||
String sql = Files.readString(path, StandardCharsets.UTF_8);
|
||||
for (String table : java.util.List.of("ll_outbound_order", "ll_outbound_pallet_item", "ll_delivery_order")) {
|
||||
var matcher = java.util.regex.Pattern.compile("(?is)CREATE TABLE(?: IF NOT EXISTS)? " + table + " \\(.*?;").matcher(sql);
|
||||
assertTrue(matcher.find(), table);
|
||||
String column = "ll_delivery_order".equals(table) ? "total_volume_cbm" : "volume_cbm";
|
||||
assertTrue(java.util.regex.Pattern.compile("(?i)" + column + " DECIMAL\\(18,4\\) NULL COMMENT").matcher(matcher.group()).find(), table + " keeps unknown volume NULL");
|
||||
}
|
||||
}
|
||||
}
|
||||
@Test
|
||||
void baselineDefinesOutboundPalletsAndDeliveryContracts() throws Exception {
|
||||
String schema = Files.readString(Path.of("src/main/resources/db/lastleg.sql"), StandardCharsets.UTF_8)
|
||||
.toLowerCase();
|
||||
|
||||
+32
@@ -311,6 +311,38 @@ class DeliveryTransferServiceTest {
|
||||
assertEquals(1, detail.skus().get(0).packageCount());
|
||||
}
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(booleans = {true, false})
|
||||
void relabelDeliveryDetailCarriesFrozenPhysicalMeasurementOnFirstSkuOnly(boolean measured) {
|
||||
DeliveryOrder delivery = new DeliveryOrder(); delivery.setId(501L); delivery.setCustomerId("TENANT-A");
|
||||
delivery.setDeliveryNo("DL-501"); delivery.setOutboundOrderId(301L); delivery.setOutboundNo("OUT-301");
|
||||
delivery.setOverseasWarehouseId("WH-1"); delivery.setCustomerName("客户快照"); delivery.setOverseasWarehouseName("仓库快照");
|
||||
delivery.setDeliveryGroupKey("RETURN_RELABEL:ABE4"); delivery.setDestinationWarehouseCode("ABE4");
|
||||
delivery.setStatus("WAIT_DELIVERY");
|
||||
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "relabel-1"); outbound.setOutboundType("RELABEL_OUTBOUND");
|
||||
OutboundPalletItem item = new OutboundPalletItem(); item.setCartonMarks("RC-1"); item.setDestinationWarehouseCode("ABE4");
|
||||
if (measured) { item.setGrossWeightKg(BigDecimal.ONE); item.setVolumeCbm(new BigDecimal("0.008")); }
|
||||
ReturnInventoryCarton carton = returnCarton("RC-1", null); carton.setId(901L);
|
||||
carton.setWeight(new BigDecimal("99")); // Reading the current carton must not replace the frozen outbound measurement.
|
||||
var first = new ReturnInventoryCartonItem(); first.setSkuCode("SKU-A"); first.setQuantity(3L);
|
||||
var second = new ReturnInventoryCartonItem(); second.setSkuCode("SKU-B"); second.setQuantity(2L);
|
||||
when(deliveryOrders.findByNo("TENANT-A", "DL-501")).thenReturn(delivery);
|
||||
when(outboundOrders.selectById(301L)).thenReturn(outbound);
|
||||
when(outboundPallets.findByOutbound("TENANT-A", 301L)).thenReturn(List.of(item));
|
||||
when(returnCartons.findCustomerMark("TENANT-A", "RC-1")).thenReturn(carton);
|
||||
when(returnCartonItems.listCarton(901L)).thenReturn(List.of(first, second));
|
||||
|
||||
var detail = service.deliveryDetail("TENANT-A", "DL-501");
|
||||
|
||||
assertEquals("客户快照", detail.order().customerName()); assertEquals("仓库快照", detail.order().overseasWarehouseName());
|
||||
assertEquals(2, detail.skus().size()); assertEquals(1, detail.skus().stream().mapToInt(row -> row.packageCount()).sum());
|
||||
assertEquals(5L, detail.skus().stream().mapToLong(row -> row.productQuantity()).sum());
|
||||
assertEquals(measured ? BigDecimal.ONE : null, detail.skus().get(0).weightKg());
|
||||
assertEquals(measured ? new BigDecimal("0.008") : null, detail.skus().get(0).volumeCbm());
|
||||
assertEquals(0, detail.skus().get(1).packageCount());
|
||||
assertNull(detail.skus().get(1).weightKg()); assertNull(detail.skus().get(1).volumeCbm());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnCartonPickDelegatesInventoryDeductionOnce() {
|
||||
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "return-1");
|
||||
|
||||
+8
@@ -1078,6 +1078,14 @@ class RelabelApplicationServiceTest {
|
||||
ReflectionTestUtils.setField(service, "bindingGuard", realGuard); ReflectionTestUtils.setField(service, "skuConversions", realConversions);
|
||||
ReflectionTestUtils.setField(service, "returnCartons", cartons);
|
||||
ReflectionTestUtils.setField(service, "deliveryOrders", org.mockito.Mockito.mock(com.baiyue.lastleg.delivery.mapper.DeliveryOrderMapper.class));
|
||||
if (!"SKU_LABEL".equals(operation)) {
|
||||
var customers = org.mockito.Mockito.mock(com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory.class);
|
||||
var warehouses = org.mockito.Mockito.mock(com.baiyue.lastleg.fleet.driver.FleetWarehouseDirectory.class);
|
||||
ReflectionTestUtils.setField(service, "customerDirectory", customers);
|
||||
ReflectionTestUtils.setField(service, "warehouseDirectory", warehouses);
|
||||
when(customers.find("10001")).thenReturn(new com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory.Customer("10001", "C", "客户", null, null));
|
||||
when(warehouses.required(900L, "OVERSEAS")).thenReturn(new com.baiyue.lastleg.fleet.driver.DriverModels.WarehouseOption("900", "WH", "海外仓", "OVERSEAS"));
|
||||
}
|
||||
|
||||
var command = new RelabelRequests.CustomerConfirm("converted-confirm", null);
|
||||
service.confirmCompletion("10001", "FBR-1", "customer-1", command);
|
||||
|
||||
+97
-15
@@ -1,6 +1,11 @@
|
||||
package com.baiyue.lastleg.relabel.service;
|
||||
|
||||
import com.baiyue.lastleg.delivery.entity.DeliveryOrder;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundOrder;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundPalletItem;
|
||||
import com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory;
|
||||
import com.baiyue.lastleg.fleet.driver.FleetWarehouseDirectory;
|
||||
import com.baiyue.lastleg.fleet.driver.DriverModels;
|
||||
import com.baiyue.lastleg.delivery.mapper.*;
|
||||
import com.baiyue.lastleg.relabel.entity.*;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||
@@ -12,13 +17,82 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.math.BigDecimal;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
|
||||
class ReturnRelabelDeliveryTest {
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings = {"EXPRESS", "PICKUP", "AMAZON_PICKUP", "CUSTOMER_PICKUP"})
|
||||
void directNativeRelabelCreatesTwoPhysicalBoxesAndOneDeliveryPerDestination(String deliveryMethod) {
|
||||
void directNativeRelabelSnapshotsNamesAndCountsMeasuredBoxesOncePerDestination(String deliveryMethod) {
|
||||
var f = fixture(deliveryMethod);
|
||||
f.create();
|
||||
verify(f.deliveries, times(2)).insert(any(DeliveryOrder.class));
|
||||
var rows = ArgumentCaptor.forClass(DeliveryOrder.class);
|
||||
verify(f.deliveries, times(2)).insert(rows.capture());
|
||||
for (var row : rows.getAllValues()) {
|
||||
assertEquals("测试客户", row.getCustomerName()); assertEquals("洛杉矶测试仓", row.getOverseasWarehouseName());
|
||||
assertEquals("100", row.getOverseasWarehouseId()); assertEquals(deliveryMethod, row.getDeliveryMethod());
|
||||
assertEquals("WAIT_DELIVERY", row.getStatus()); assertEquals(f.application.getOutboundOrderNo(), row.getOutboundNo());
|
||||
int boxes = "A".equals(row.getDestinationWarehouseCode()) ? 2 : 1;
|
||||
assertEquals(boxes, row.getPackageCount());
|
||||
assertEquals(0, BigDecimal.valueOf(boxes).compareTo(row.getTotalWeightKg()));
|
||||
assertEquals(0, new BigDecimal("0.008").multiply(BigDecimal.valueOf(boxes)).compareTo(row.getTotalVolumeCbm()));
|
||||
}
|
||||
var outbound = ArgumentCaptor.forClass(OutboundOrder.class);
|
||||
verify(f.outbounds).insert(outbound.capture());
|
||||
assertEquals(3, outbound.getValue().getPackageCount());
|
||||
assertEquals(0, new BigDecimal("3").compareTo(outbound.getValue().getGrossWeightKg()));
|
||||
assertEquals(0, new BigDecimal("0.024").compareTo(outbound.getValue().getVolumeCbm()));
|
||||
var physical = ArgumentCaptor.forClass(OutboundPalletItem.class);
|
||||
verify(f.outboundItems, times(3)).insert(physical.capture());
|
||||
assertTrue(physical.getAllValues().stream().allMatch(row -> row.getPackageCount() == 1
|
||||
&& BigDecimal.ONE.compareTo(row.getGrossWeightKg()) == 0 && new BigDecimal("0.008").compareTo(row.getVolumeCbm()) == 0));
|
||||
verify(f.customers, times(1)).find("C1"); verify(f.warehouses, times(1)).required(100L, "OVERSEAS");
|
||||
}
|
||||
|
||||
@Test
|
||||
void incompleteMeasurementStaysUnknownWithoutBlockingAndDoesNotContaminateOtherDestination() {
|
||||
var f = fixture("AMAZON_PICKUP");
|
||||
f.cartonRows.get(0).setWeight(null); f.cartonRows.get(0).setHeight(null);
|
||||
f.create();
|
||||
var rows = ArgumentCaptor.forClass(DeliveryOrder.class); verify(f.deliveries, times(2)).insert(rows.capture());
|
||||
var a = rows.getAllValues().stream().filter(row -> "A".equals(row.getDestinationWarehouseCode())).findFirst().orElseThrow();
|
||||
var b = rows.getAllValues().stream().filter(row -> "B".equals(row.getDestinationWarehouseCode())).findFirst().orElseThrow();
|
||||
assertNull(a.getTotalWeightKg()); assertNull(a.getTotalVolumeCbm());
|
||||
assertEquals(0, BigDecimal.ONE.compareTo(b.getTotalWeightKg()));
|
||||
assertEquals(0, new BigDecimal("0.008").compareTo(b.getTotalVolumeCbm()));
|
||||
var outbound = ArgumentCaptor.forClass(OutboundOrder.class); verify(f.outbounds).insert(outbound.capture());
|
||||
assertNull(outbound.getValue().getGrossWeightKg()); assertNull(outbound.getValue().getVolumeCbm());
|
||||
}
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings = {"customer-name", "customer-id", "warehouse-name", "warehouse-id", "unavailable"})
|
||||
void invalidDirectorySnapshotDoesNotCreateAnyDocument(String failure) {
|
||||
var f = fixture("CUSTOMER_PICKUP");
|
||||
if (failure.startsWith("customer")) when(f.customers.find("C1")).thenReturn(new FleetCustomerDirectory.Customer(
|
||||
"customer-id".equals(failure) ? "OTHER" : "C1", "C", "customer-name".equals(failure) ? " " : "客户", null, null));
|
||||
else if (failure.startsWith("warehouse")) when(f.warehouses.required(100L, "OVERSEAS")).thenReturn(new DriverModels.WarehouseOption(
|
||||
"warehouse-id".equals(failure) ? "200" : "100", "WH", "warehouse-name".equals(failure) ? " " : "仓库", "OVERSEAS"));
|
||||
else when(f.customers.find("C1")).thenThrow(new ResponseStatusException(org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE));
|
||||
assertThrows(ResponseStatusException.class, f::create);
|
||||
verify(f.outbounds, never()).insert(any(OutboundOrder.class));
|
||||
verifyNoInteractions(f.deliveries, f.outboundItems);
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingOutboundReplayDoesNotRefreshNamesOrMeasurements() {
|
||||
var f = fixture("PICKUP"); var existing = new OutboundOrder(); existing.setOutboundNo("OUT-EXISTING");
|
||||
when(f.outbounds.findByIdempotency("C1", "RELABEL:1:OUTBOUND")).thenReturn(existing);
|
||||
f.create();
|
||||
assertEquals("OUT-EXISTING", f.application.getOutboundOrderNo());
|
||||
verifyNoInteractions(f.customers, f.warehouses, f.deliveries, f.outboundItems);
|
||||
}
|
||||
|
||||
private Fixture fixture(String deliveryMethod) {
|
||||
var service = new RelabelApplicationService();
|
||||
var items = mock(RelabelItemMapper.class);
|
||||
var cartons = mock(ReturnInventoryCartonMapper.class);
|
||||
@@ -26,28 +100,36 @@ class ReturnRelabelDeliveryTest {
|
||||
ReflectionTestUtils.setField(service, "items", items);
|
||||
ReflectionTestUtils.setField(service, "returnCartons", cartons);
|
||||
ReflectionTestUtils.setField(service, "deliveryOrders", deliveries);
|
||||
ReflectionTestUtils.setField(service, "outboundOrders", mock(OutboundOrderMapper.class));
|
||||
ReflectionTestUtils.setField(service, "outboundItems", mock(OutboundPalletItemMapper.class));
|
||||
var outbounds = mock(OutboundOrderMapper.class); var outboundItems = mock(OutboundPalletItemMapper.class);
|
||||
ReflectionTestUtils.setField(service, "outboundOrders", outbounds);
|
||||
ReflectionTestUtils.setField(service, "outboundItems", outboundItems);
|
||||
var customers = mock(FleetCustomerDirectory.class); var warehouses = mock(FleetWarehouseDirectory.class);
|
||||
ReflectionTestUtils.setField(service, "customerDirectory", customers); ReflectionTestUtils.setField(service, "warehouseDirectory", warehouses);
|
||||
when(customers.find("C1")).thenReturn(new FleetCustomerDirectory.Customer("C1", "C", "测试客户", null, null));
|
||||
when(warehouses.required(100L, "OVERSEAS")).thenReturn(new DriverModels.WarehouseOption("100", "WH", "洛杉矶测试仓", "OVERSEAS"));
|
||||
ReflectionTestUtils.setField(service, "outboundTasks", mock(OutboundPdaTaskMapper.class));
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||
var application = new RelabelApplication(); application.setId(1L); application.setCustomerId("C1");
|
||||
application.setRelabelNo("REL1"); application.setOverseasWarehouseId("WH1"); application.setDeliveryMethod(deliveryMethod);
|
||||
var a = source(10L, "A"); var b = source(11L, "B");
|
||||
when(items.selectList(any())).thenReturn(List.of(a, b));
|
||||
for (var item : List.of(a, b)) {
|
||||
application.setRelabelNo("REL1"); application.setOverseasWarehouseId("100"); application.setDeliveryMethod(deliveryMethod);
|
||||
var sources = List.of(source(10L, "A"), source(11L, "B"), source(12L, "A"));
|
||||
when(items.selectList(any())).thenReturn(sources);
|
||||
var cartonRows = new java.util.ArrayList<ReturnInventoryCarton>();
|
||||
for (var item : sources) {
|
||||
var carton = new ReturnInventoryCarton(); carton.setId(item.getReturnCartonId());
|
||||
carton.setCustomerId("C1"); carton.setRelabelApplicationId(1L); carton.setStatus("RELABEL_PENDING");
|
||||
carton.setOverseasWarehouseId("WH1");
|
||||
carton.setOverseasWarehouseId("100"); carton.setCountStatus("CONFIRMED"); carton.setSourceUnitType("PALLET");
|
||||
carton.setWeight(BigDecimal.ONE); carton.setLength(new BigDecimal("20"));
|
||||
carton.setWidth(new BigDecimal("20")); carton.setHeight(new BigDecimal("20"));
|
||||
carton.setTotalQuantity(5L); carton.setSkuTypes(2); cartonRows.add(carton);
|
||||
when(cartons.lockId(carton.getId())).thenReturn(carton);
|
||||
}
|
||||
ReflectionTestUtils.invokeMethod(service, "createDirectOutbound", application, "OP1", LocalDateTime.now());
|
||||
verify(deliveries, times(2)).insert(any(DeliveryOrder.class));
|
||||
verify(deliveries).insert(argThat((DeliveryOrder d) -> "A".equals(d.getDestinationWarehouseCode())
|
||||
&& d.getPackageCount() == 1 && "WAIT_DELIVERY".equals(d.getStatus())
|
||||
&& deliveryMethod.equals(d.getDeliveryMethod())
|
||||
&& application.getOutboundOrderNo().equals(d.getOutboundNo())));
|
||||
verify(deliveries).insert(argThat((DeliveryOrder d) -> "B".equals(d.getDestinationWarehouseCode()) && d.getPackageCount() == 1));
|
||||
return new Fixture(service, application, customers, warehouses, deliveries, outbounds, outboundItems, cartonRows);
|
||||
}
|
||||
private record Fixture(RelabelApplicationService service, RelabelApplication application, FleetCustomerDirectory customers,
|
||||
FleetWarehouseDirectory warehouses, DeliveryOrderMapper deliveries, OutboundOrderMapper outbounds,
|
||||
OutboundPalletItemMapper outboundItems, List<ReturnInventoryCarton> cartonRows) {
|
||||
void create() { ReflectionTestUtils.invokeMethod(service, "createDirectOutbound", application, "OP1", LocalDateTime.now()); }
|
||||
}
|
||||
private RelabelItem source(long id, String destination) {
|
||||
var item = new RelabelItem(); item.setId(id); item.setReturnCartonId(id);
|
||||
|
||||
Reference in New Issue
Block a user