fix(lastleg): route all relabel pickups directly to outbound and verify final schemas
This commit is contained in:
+6
-1
@@ -13,7 +13,12 @@ class AppointmentWorkSchemaContractTest {
|
||||
String target=Files.readString(Path.of(path));
|
||||
for(String table:List.of("ll_dispatch_appointment_allocation","ll_appointment_work_commitment","ll_appointment_work_guard","ll_appointment_work","ll_appointment_work_history"))
|
||||
assertEquals(definition(source,table),definition(target,table),path+" differs for "+table);
|
||||
assertTrue(target.contains("INSERT INTO ll_appointment_work_guard(id,initialized) SELECT 1,FALSE"));
|
||||
if (path.startsWith("../deploy/")) {
|
||||
assertTrue(target.contains("INSERT INTO ll_appointment_work_guard (id,initialized) VALUES (1,FALSE);"));
|
||||
assertFalse(target.contains("WHERE NOT EXISTS(SELECT 1 FROM ll_appointment_work_guard"));
|
||||
} else {
|
||||
assertTrue(target.contains("INSERT INTO ll_appointment_work_guard(id,initialized) SELECT 1,FALSE"));
|
||||
}
|
||||
}
|
||||
assertTrue(source.contains("scan_cursor VARCHAR(128)"));
|
||||
assertTrue(source.contains("PRIMARY KEY(dispatch_item_id,batch_key,source_key,destination_key)"));
|
||||
|
||||
+26
@@ -16,6 +16,32 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class DispatchCandidateMapperContractTest {
|
||||
@Test
|
||||
void ordinaryAndReturnRelabelCandidatesExcludeDirectOutboundDeliveryModes() throws Exception {
|
||||
for (String method : List.of("relabelAvailable", "nativeReturnRelabelAvailable")) {
|
||||
String sql = String.join(" ", DispatchCandidateMapper.class
|
||||
.getMethod(method, String.class, String.class, String.class).getAnnotation(Select.class).value());
|
||||
String eligibility = sql.substring(sql.indexOf("WHERE a.delivery_method"), sql.indexOf("<if test='customerId"));
|
||||
try (var connection = DriverManager.getConnection("jdbc:h2:mem:relabel-mode-" + method + ";MODE=MySQL")) {
|
||||
var statement = connection.createStatement();
|
||||
statement.execute("CREATE TABLE ll_relabel_application(delivery_method VARCHAR,status VARCHAR,fulfillment_status VARCHAR)");
|
||||
statement.execute("INSERT INTO ll_relabel_application VALUES "
|
||||
+ "('TRUCK','READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH'),"
|
||||
+ "('PICKUP','READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH'),"
|
||||
+ "('AMAZON_PICKUP','READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH'),"
|
||||
+ "('CUSTOMER_PICKUP','READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH'),"
|
||||
+ "('UNKNOWN','READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH'),"
|
||||
+ "(NULL,'READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH'),"
|
||||
+ "('EXPRESS','READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH')");
|
||||
// The return query also requires physical return-carton availability.
|
||||
eligibility = eligibility.replace("AND c.status='RELABEL_PENDING' AND c.pallet_code IS NOT NULL", "");
|
||||
try (var rows = statement.executeQuery("SELECT delivery_method FROM ll_relabel_application a " + eligibility)) {
|
||||
assertTrue(rows.next()); assertEquals("TRUCK", rows.getString(1)); assertFalse(rows.next());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmedAppointmentUsesOriginalCustomerOrderAndDestinationAndNeverInventsLoadingTime() throws Exception {
|
||||
try (var connection = DriverManager.getConnection("jdbc:h2:mem:dispatch-confirmed-appointment;MODE=MySQL")) {
|
||||
|
||||
+19
@@ -51,6 +51,25 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DispatchServiceTest {
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.NullAndEmptySource
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings = {"PICKUP", "EXPRESS", "AMAZON_PICKUP", "CUSTOMER_PICKUP", "UNKNOWN"})
|
||||
void directRelabelCannotResolveDispatchScopeOrBypassCandidateFiltering(String deliveryMethod) {
|
||||
var relabels = mock(com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper.class);
|
||||
ReflectionTestUtils.setField(service, "relabelApplications", relabels);
|
||||
var application = new com.baiyue.lastleg.relabel.entity.RelabelApplication();
|
||||
application.setCustomerId("100"); application.setStatus("READY_FOR_OUTBOUND");
|
||||
application.setFulfillmentStatus("WAIT_PRE_DISPATCH"); application.setDeliveryMethod(deliveryMethod);
|
||||
when(relabels.findAnyByRelabelNo("RL-1")).thenReturn(application);
|
||||
when(relabels.lockNo("100", "RL-1")).thenReturn(application);
|
||||
assertThrows(ResponseStatusException.class, () -> service.resolveCustomerScopeForRelabel("RL-1"));
|
||||
assertThrows(ResponseStatusException.class, () -> service.preArrange("100", "OP",
|
||||
new DispatchRequests.PreArrange(List.of(1L), null, null, null, null, "RL-1")));
|
||||
verify(orders, never()).insert(any(DispatchOrder.class));
|
||||
verify(candidates, never()).relabelAvailable(any(), any(), any());
|
||||
verify(relabelMeasurements, never()).requireMeasured(any());
|
||||
}
|
||||
|
||||
@Mock DispatchOrderMapper orders;
|
||||
@Mock DispatchItemMapper items;
|
||||
@Mock DispatchBolMapper bolLines;
|
||||
|
||||
+14
@@ -44,6 +44,20 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PdaRelabelServiceTest {
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings = {"PICKUP", "EXPRESS", "TRUCK", "AMAZON_PICKUP", "CUSTOMER_PICKUP"})
|
||||
void bindingRequirementDistinguishesOrdinaryInventoryFromDirectReturnCartons(String deliveryMethod) {
|
||||
application.setDeliveryMethod(deliveryMethod); item.setOperationType("CARTON_LABEL");
|
||||
when(applications.findAnyByRelabelNo("FBR-1")).thenReturn(application);
|
||||
assertTrue(service.detail("7", 900L, "FBR-1").requiresPalletBinding());
|
||||
var cartons = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper.class);
|
||||
ReflectionTestUtils.setField(service, "returnCartons", cartons);
|
||||
item.setReturnCartonId(40L);
|
||||
assertEquals("TRUCK".equals(deliveryMethod), service.detail("7", 900L, "FBR-1").requiresPalletBinding());
|
||||
application.setDeliveryMethod(null); item.setOperationType("SKU_LABEL");
|
||||
assertEquals(false, service.detail("7", 900L, "FBR-1").requiresPalletBinding());
|
||||
}
|
||||
|
||||
@Mock RelabelApplicationMapper applications;
|
||||
@Mock RelabelItemMapper items;
|
||||
@Mock PdaRelabelTaskMapper tasks;
|
||||
|
||||
+96
-3
@@ -192,6 +192,7 @@ class RelabelApplicationServiceTest {
|
||||
}
|
||||
@Mock RelabelApplicationMapper applications;
|
||||
@Mock RelabelItemMapper items;
|
||||
@Mock RelabelBindingGuard bindingGuard;
|
||||
@Mock RelabelStatusHistoryMapper histories;
|
||||
@Mock LastlegOrderMapper orders;
|
||||
@Mock LastlegOrderStatusHistoryMapper orderHistories;
|
||||
@@ -214,6 +215,7 @@ class RelabelApplicationServiceTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new RelabelApplicationService();
|
||||
ReflectionTestUtils.setField(service, "bindingGuard", bindingGuard);
|
||||
ReflectionTestUtils.setField(service, "returnPackingTraces", org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper.class));
|
||||
ReflectionTestUtils.setField(service, "skuConversions", org.mockito.Mockito.mock(com.baiyue.lastleg.relabel.service.ReturnSkuConversionService.class));
|
||||
ReflectionTestUtils.setField(service, "skuConversionRows", org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnSkuConversionMapper.class));
|
||||
@@ -970,9 +972,11 @@ class RelabelApplicationServiceTest {
|
||||
verify(applications, never()).updateById(application);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pickupCustomerConfirmationCreatesOneExactCartonOutbound() {
|
||||
RelabelApplication application = confirmationApplication("PICKUP", false);
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.CsvSource({"PICKUP,false", "EXPRESS,false", "AMAZON_PICKUP,false", "CUSTOMER_PICKUP,false",
|
||||
"PICKUP,true", "EXPRESS,true", "AMAZON_PICKUP,true", "CUSTOMER_PICKUP,true"})
|
||||
void directCustomerConfirmationCreatesOneExactCartonOutboundAndReplays(String deliveryMethod, boolean linkedTransfer) {
|
||||
RelabelApplication application = confirmationApplication(deliveryMethod, false);
|
||||
RelabelItem source = item(31L, application.getId(), "SKU-A", 1, 1);
|
||||
source.setSourceInventoryPositionId(201L);
|
||||
source.setSourceInboundCode("CODE-1");
|
||||
@@ -980,13 +984,21 @@ class RelabelApplicationServiceTest {
|
||||
source.setDestinationFbaWarehouseCode("ONT8");
|
||||
InventoryPosition position = new InventoryPosition();
|
||||
position.setId(201L); position.setCustomerId("10001"); position.setPalletId(501L); position.setPalletNo("PALLET-1");
|
||||
position.setOverseasWarehouseId("900");
|
||||
when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
|
||||
when(applications.findNo("10001", "FBR-1")).thenReturn(application);
|
||||
when(items.selectList(any())).thenReturn(List.of(source));
|
||||
when(histories.selectList(any())).thenReturn(List.of());
|
||||
when(orders.selectById(21L)).thenReturn(order(21L, "10001", "LMO-RELABEL-1", "PENDING_ASSIGNMENT"));
|
||||
when(inventoryPositions.selectById(201L)).thenReturn(position);
|
||||
if (linkedTransfer) {
|
||||
var transfer = new WarehouseTransferOrder(); transfer.setId(77L); transfer.setCustomerId("10001");
|
||||
transfer.setStatus("WAIT_RELABEL"); transfer.setLastlegOrderId(88L); transfer.setFbaWarehouseCode("ONT8");
|
||||
when(warehouseTransfers.lockByRelabelNo("10001", "FBR-1")).thenReturn(transfer);
|
||||
}
|
||||
|
||||
service.confirmCompletion("10001", "FBR-1", "customer-1",
|
||||
new RelabelRequests.CustomerConfirm("confirm-pickup", null));
|
||||
service.confirmCompletion("10001", "FBR-1", "customer-1",
|
||||
new RelabelRequests.CustomerConfirm("confirm-pickup", null));
|
||||
|
||||
@@ -1000,6 +1012,87 @@ class RelabelApplicationServiceTest {
|
||||
assertEquals("BOX-1", outboundItem.getValue().getCartonMarks());
|
||||
assertEquals("PALLET-1", outboundItem.getValue().getPalletNo());
|
||||
verify(outboundTasks, times(1)).insert(any(OutboundPdaTask.class));
|
||||
verify(bindingGuard, times(1)).requireBound(application);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customerConfirmationRejectsUnboundManualCompletionBeforeAnyWrite() {
|
||||
RelabelApplication application = confirmationApplication("PICKUP", false);
|
||||
when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
|
||||
org.mockito.Mockito.doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT,
|
||||
"箱唛 BOX-1 尚未完成托盘绑定")).when(bindingGuard).requireBound(application);
|
||||
assertThrows(ResponseStatusException.class, () -> service.confirmCompletion("10001", "FBR-1", "customer-1",
|
||||
new RelabelRequests.CustomerConfirm("manual-confirm", null)));
|
||||
verify(pdaTasks, never()).updateWithVersion(any(), any());
|
||||
verify(applications, never()).updateById(any(RelabelApplication.class));
|
||||
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
|
||||
verify(outboundItems, never()).insert(any(OutboundPalletItem.class));
|
||||
verify(outboundTasks, never()).insert(any(OutboundPdaTask.class));
|
||||
assertEquals(RelabelApplicationService.CUSTOMER_CONFIRM_PENDING, application.getStatus());
|
||||
}
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.CsvSource({"SKU_LABEL,", "CARTON_AND_SKU,PICKUP", "CARTON_AND_SKU,EXPRESS",
|
||||
"CARTON_AND_SKU,AMAZON_PICKUP", "CARTON_AND_SKU,CUSTOMER_PICKUP"})
|
||||
void convertedReturnContentsPassRealConfirmationGuardAndRemainValidAfterConfirmation(String operation, String deliveryMethod) {
|
||||
var application = confirmationApplication(deliveryMethod, false);
|
||||
var source = item(31L, application.getId(), "OLD-A", 1, 1);
|
||||
source.setOperationType(operation); source.setReturnCartonId(301L); source.setSourceInventoryPositionId(null);
|
||||
source.setCartonMark("BOX-1"); source.setSourceCartonMark("RETURN-BOX-1");
|
||||
source.setDestinationFbaWarehouseCode("ONT8");
|
||||
source.setSkuRelabelItemsJson("[{\"skuCode\":\"OLD-A\",\"newSkuCode\":\"NEW\",\"quantity\":3},{\"skuCode\":\"OLD-B\",\"newSkuCode\":\"NEW\",\"quantity\":2}]");
|
||||
when(items.selectList(any())).thenReturn(List.of(source));
|
||||
when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
|
||||
when(applications.findNo("10001", "FBR-1")).thenReturn(application);
|
||||
when(orders.selectById(21L)).thenReturn(order(21L, "10001", "LMO-RELABEL-1", "WAIT_RELABEL"));
|
||||
var cartons = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper.class);
|
||||
var contents = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper.class);
|
||||
var conversions = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnSkuConversionMapper.class);
|
||||
var pdaHistories = org.mockito.Mockito.mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper.class);
|
||||
var completed = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory();
|
||||
completed.setAction("COMPLETE"); completed.setDetailJson("{\"roundKey\":\"INITIAL\"}");
|
||||
when(pdaHistories.selectList(any())).thenReturn(List.of(completed));
|
||||
var carton = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton();
|
||||
carton.setId(301L); carton.setCustomerId("10001"); carton.setOverseasWarehouseId("900");
|
||||
carton.setRelabelApplicationId(application.getId()); carton.setStatus("RELABEL_PENDING");
|
||||
carton.setCountStatus("CONFIRMED"); carton.setSourceUnitType("BOX"); carton.setTotalQuantity(5L);
|
||||
lenient().when(cartons.selectById(301L)).thenReturn(carton);
|
||||
when(cartons.lockId(301L)).thenReturn(carton);
|
||||
var content = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem();
|
||||
content.setSkuCode("NEW"); content.setQuantity(5L);
|
||||
when(contents.lockCarton(301L)).thenReturn(List.of(content));
|
||||
var rows = new java.util.ArrayList<com.baiyue.lastleg.returnreceipt.entity.ReturnSkuConversion>();
|
||||
for (var oldSku : List.of("OLD-A", "OLD-B")) {
|
||||
var row = new com.baiyue.lastleg.returnreceipt.entity.ReturnSkuConversion();
|
||||
row.setCustomerId("10001"); row.setApplicationId(application.getId()); row.setItemId(source.getId());
|
||||
row.setCartonId(301L); row.setRoundKey("INITIAL"); row.setStatus("APPLIED");
|
||||
row.setOldSkuCode(oldSku); row.setNewSkuCode("NEW"); row.setQuantity("OLD-A".equals(oldSku) ? 3L : 2L);
|
||||
rows.add(row);
|
||||
}
|
||||
when(conversions.lockApplication("10001", application.getId())).thenReturn(rows);
|
||||
var realGuard = new RelabelBindingGuard();
|
||||
ReflectionTestUtils.setField(realGuard, "items", items); ReflectionTestUtils.setField(realGuard, "histories", pdaHistories);
|
||||
ReflectionTestUtils.setField(realGuard, "returnCartons", cartons); ReflectionTestUtils.setField(realGuard, "returnLines", contents);
|
||||
ReflectionTestUtils.setField(realGuard, "conversions", conversions); ReflectionTestUtils.setField(realGuard, "objectMapper", mapper());
|
||||
var realConversions = new ReturnSkuConversionService(); ReflectionTestUtils.setField(realConversions, "conversions", conversions);
|
||||
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));
|
||||
|
||||
var command = new RelabelRequests.CustomerConfirm("converted-confirm", null);
|
||||
service.confirmCompletion("10001", "FBR-1", "customer-1", command);
|
||||
service.confirmCompletion("10001", "FBR-1", "customer-1", command);
|
||||
assertTrue(rows.stream().allMatch(row -> "CONFIRMED".equals(row.getStatus())));
|
||||
realGuard.requireBound(application);
|
||||
verify(conversions, times(2)).updateById(any(com.baiyue.lastleg.returnreceipt.entity.ReturnSkuConversion.class));
|
||||
if ("SKU_LABEL".equals(operation)) {
|
||||
assertEquals("COMPLETED", application.getStatus()); assertEquals("NOT_REQUIRED", application.getFulfillmentStatus());
|
||||
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
|
||||
} else {
|
||||
assertEquals("OUTBOUND_CREATED", application.getFulfillmentStatus());
|
||||
verify(outboundOrders, times(1)).insert(any(OutboundOrder.class));
|
||||
assertEquals("OUTBOUND_PENDING", carton.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
private RelabelApplication confirmationApplication(String deliveryMethod, boolean printShippingLabel) {
|
||||
|
||||
+107
-4
@@ -17,7 +17,74 @@ import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class RelabelBindingGuardTest {
|
||||
@Test void parcelMixedCompletionRechecksAllContentsWithoutRequiringExpressPallet() {
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings = {"SKU_LABEL", "CARTON_AND_SKU", "RETURN_BULK_SKU_LABEL"})
|
||||
void convertedContentsRequireCurrentCustomerApplicationItemCartonAndRound(String operation) {
|
||||
var guard = new RelabelBindingGuard();
|
||||
var items = mock(RelabelItemMapper.class); var history = mock(PdaRelabelHistoryMapper.class);
|
||||
var cartons = mock(ReturnInventoryCartonMapper.class);
|
||||
var contents = mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper.class);
|
||||
var conversions = mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnSkuConversionMapper.class);
|
||||
ReflectionTestUtils.setField(guard, "items", items); ReflectionTestUtils.setField(guard, "histories", history);
|
||||
ReflectionTestUtils.setField(guard, "returnCartons", cartons); ReflectionTestUtils.setField(guard, "returnLines", contents);
|
||||
ReflectionTestUtils.setField(guard, "conversions", conversions); ReflectionTestUtils.setField(guard, "objectMapper", new ObjectMapper());
|
||||
var app = new RelabelApplication(); app.setId(1L); app.setCustomerId("2"); app.setOverseasWarehouseId("W");
|
||||
app.setStatus("CUSTOMER_CONFIRM_PENDING"); app.setDeliveryMethod("PICKUP");
|
||||
var item = new RelabelItem(); item.setId(4L); item.setReturnCartonId(3L); item.setOperationType(operation);
|
||||
item.setCartonMark("BOX"); item.setSkuCode("A"); item.setNewSkuCode("B"); item.setNotifiedPackageCount(3);
|
||||
item.setSkuRelabelItemsJson("[{\"skuCode\":\"A\",\"newSkuCode\":\"B\",\"quantity\":3}]");
|
||||
var carton = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton();
|
||||
carton.setCustomerId("2"); carton.setOverseasWarehouseId("W");
|
||||
var complete = new PdaRelabelHistory(); complete.setAction("COMPLETE"); complete.setDetailJson("{\"roundKey\":\"ROUND-2\"}");
|
||||
var line = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem(); line.setSkuCode("B"); line.setQuantity(3L);
|
||||
var row = conversion("A", "B", 3L);
|
||||
when(items.selectList(any())).thenReturn(List.of(item)); when(history.selectList(any())).thenReturn(List.of(complete));
|
||||
when(cartons.selectById(3L)).thenReturn(carton); when(contents.lockCarton(3L)).thenReturn(List.of(line));
|
||||
when(conversions.lockApplication("2", 1L)).thenReturn(List.of(row));
|
||||
assertDoesNotThrow(() -> guard.requireBound(app));
|
||||
row.setStatus("CONFIRMED"); app.setStatus("READY_FOR_OUTBOUND");
|
||||
assertDoesNotThrow(() -> guard.requireBound(app));
|
||||
row.setCustomerId("OTHER"); assertThrows(ResponseStatusException.class, () -> guard.requireBound(app)); row.setCustomerId("2");
|
||||
row.setApplicationId(9L); assertThrows(ResponseStatusException.class, () -> guard.requireBound(app)); row.setApplicationId(1L);
|
||||
row.setItemId(9L); assertThrows(ResponseStatusException.class, () -> guard.requireBound(app)); row.setItemId(4L);
|
||||
row.setCartonId(9L); assertThrows(ResponseStatusException.class, () -> guard.requireBound(app)); row.setCartonId(3L);
|
||||
row.setRoundKey("ROUND-1"); assertThrows(ResponseStatusException.class, () -> guard.requireBound(app)); row.setRoundKey("ROUND-2");
|
||||
row.setStatus("REVERSED"); assertThrows(ResponseStatusException.class, () -> guard.requireBound(app)); row.setStatus("APPLIED");
|
||||
line.setQuantity(2L); assertThrows(ResponseStatusException.class, () -> guard.requireBound(app)); line.setQuantity(3L);
|
||||
carton.setOverseasWarehouseId("OTHER"); assertThrows(ResponseStatusException.class, () -> guard.requireBound(app)); carton.setOverseasWarehouseId("W");
|
||||
carton.setCustomerId("OTHER"); assertThrows(ResponseStatusException.class, () -> guard.requireBound(app)); carton.setCustomerId("2");
|
||||
when(conversions.lockApplication("2", 1L)).thenReturn(List.of());
|
||||
assertThrows(ResponseStatusException.class, () -> guard.requireBound(app));
|
||||
when(conversions.lockApplication("2", 1L)).thenReturn(List.of(row));
|
||||
var archive = new PdaRelabelHistory(); archive.setAction("REWORK_ARCHIVE");
|
||||
when(history.selectList(any())).thenReturn(List.of(complete, archive));
|
||||
assertThrows(ResponseStatusException.class, () -> guard.requireBound(app));
|
||||
|
||||
// A -> B and original B -> C must subtract both original quantities before adding targets.
|
||||
when(history.selectList(any())).thenReturn(List.of(complete));
|
||||
item.setOperationType("CARTON_AND_SKU");
|
||||
item.setSkuRelabelItemsJson("[{\"skuCode\":\"A\",\"newSkuCode\":\"B\",\"quantity\":3},{\"skuCode\":\"B\",\"newSkuCode\":\"C\",\"quantity\":2}]");
|
||||
var second = conversion("B", "C", 2L);
|
||||
var target = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem(); target.setSkuCode("C"); target.setQuantity(2L);
|
||||
when(contents.lockCarton(3L)).thenReturn(List.of(line, target));
|
||||
when(conversions.lockApplication("2", 1L)).thenReturn(List.of(second, row));
|
||||
assertDoesNotThrow(() -> guard.requireBound(app));
|
||||
when(conversions.lockApplication("2", 1L)).thenReturn(List.of(row, second));
|
||||
assertDoesNotThrow(() -> guard.requireBound(app));
|
||||
second.setQuantity(5L); line.setQuantity(0L); target.setQuantity(5L);
|
||||
assertThrows(ResponseStatusException.class, () -> guard.requireBound(app), "不能把新转入 B 当作本申请原批准 B 数量");
|
||||
}
|
||||
|
||||
private com.baiyue.lastleg.returnreceipt.entity.ReturnSkuConversion conversion(String oldSku, String newSku, long quantity) {
|
||||
var row = new com.baiyue.lastleg.returnreceipt.entity.ReturnSkuConversion();
|
||||
row.setCustomerId("2"); row.setApplicationId(1L); row.setItemId(4L); row.setCartonId(3L);
|
||||
row.setRoundKey("ROUND-2"); row.setStatus("APPLIED"); row.setOldSkuCode(oldSku); row.setNewSkuCode(newSku); row.setQuantity(quantity);
|
||||
return row;
|
||||
}
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings = {"EXPRESS", "PICKUP", "AMAZON_PICKUP", "CUSTOMER_PICKUP"})
|
||||
void parcelMixedCompletionRechecksAllContentsWithoutRequiringDirectOutboundPallet(String deliveryMethod) {
|
||||
var guard = new RelabelBindingGuard();
|
||||
var items = mock(RelabelItemMapper.class); var histories = mock(PdaRelabelHistoryMapper.class);
|
||||
var cartons = mock(ReturnInventoryCartonMapper.class);
|
||||
@@ -25,7 +92,7 @@ class RelabelBindingGuardTest {
|
||||
ReflectionTestUtils.setField(guard, "items", items); ReflectionTestUtils.setField(guard, "histories", histories);
|
||||
ReflectionTestUtils.setField(guard, "returnCartons", cartons); ReflectionTestUtils.setField(guard, "returnLines", lines);
|
||||
ReflectionTestUtils.setField(guard, "objectMapper", new ObjectMapper());
|
||||
var app = new RelabelApplication(); app.setId(1L); app.setCustomerId("2"); app.setDeliveryMethod("EXPRESS");
|
||||
var app = new RelabelApplication(); app.setId(1L); app.setCustomerId("2"); app.setDeliveryMethod(deliveryMethod);
|
||||
var item = new RelabelItem(); item.setReturnCartonId(3L); item.setOperationType("CARTON_AND_SKU");
|
||||
item.setCartonMark("PARCEL-1"); item.setSkuRelabelItemsJson("[{\"skuCode\":\"A\",\"quantity\":3}]");
|
||||
var parcel = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton(); parcel.setCustomerId("2"); parcel.setSourceUnitType("PARCEL");
|
||||
@@ -51,14 +118,47 @@ class RelabelBindingGuardTest {
|
||||
item.setSkuRelabelItemsJson("[{\"skuCode\":\"A\",\"newSkuCode\":\"NEW-A\",\"quantity\":3}]");
|
||||
when(items.selectList(any())).thenReturn(List.of(item)); when(histories.selectList(any())).thenReturn(List.of());
|
||||
var stock = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem(); stock.setSkuCode("A"); stock.setQuantity(10L);
|
||||
var ownedCarton = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton(); ownedCarton.setCustomerId("2");
|
||||
when(cartons.selectById(3L)).thenReturn(ownedCarton);
|
||||
when(lines.lockCarton(3L)).thenReturn(List.of(stock));
|
||||
assertThrows(ResponseStatusException.class, () -> guard.requireBound(app));
|
||||
stock.setQuantity(3L);
|
||||
assertDoesNotThrow(() -> guard.requireBound(app));
|
||||
assertNull(ownedCarton.getPalletCode(), "纯 SKU 留仓不要求托盘,但仍校验来源归属");
|
||||
stock.setSkuCode("OTHER");
|
||||
assertThrows(ResponseStatusException.class, () -> guard.requireBound(app));
|
||||
stock.setSkuCode("A");
|
||||
app.setDeliveryMethod("EXPRESS"); assertDoesNotThrow(() -> guard.requireBound(app));
|
||||
app.setDeliveryMethod("PICKUP"); assertDoesNotThrow(() -> guard.requireBound(app));
|
||||
app.setDeliveryMethod("AMAZON_PICKUP"); assertDoesNotThrow(() -> guard.requireBound(app));
|
||||
app.setDeliveryMethod("CUSTOMER_PICKUP"); assertDoesNotThrow(() -> guard.requireBound(app));
|
||||
app.setDeliveryMethod("TRUCK"); assertThrows(ResponseStatusException.class, () -> guard.requireBound(app));
|
||||
}
|
||||
@Test void physicalBindingMustMatchLatestTargetAndCannotSurviveRework() {
|
||||
@Test void returnBulkSkuWithoutDeliveryDoesNotBypassPalletBinding() {
|
||||
var guard = new RelabelBindingGuard();
|
||||
var items = mock(RelabelItemMapper.class); var histories = mock(PdaRelabelHistoryMapper.class);
|
||||
var lines = mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper.class);
|
||||
var cartons = mock(ReturnInventoryCartonMapper.class);
|
||||
ReflectionTestUtils.setField(guard, "items", items); ReflectionTestUtils.setField(guard, "histories", histories);
|
||||
ReflectionTestUtils.setField(guard, "returnLines", lines); ReflectionTestUtils.setField(guard, "returnCartons", cartons);
|
||||
ReflectionTestUtils.setField(guard, "objectMapper", new ObjectMapper());
|
||||
var app = new RelabelApplication(); app.setId(1L); app.setCustomerId("2");
|
||||
var item = new RelabelItem(); item.setReturnCartonId(3L); item.setCartonMark("RETURN-BOX");
|
||||
item.setOperationType("RETURN_BULK_SKU_LABEL"); item.setSkuCode("A"); item.setNotifiedPackageCount(3);
|
||||
var stock = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem();
|
||||
stock.setSkuCode("A"); stock.setQuantity(3L);
|
||||
when(items.selectList(any())).thenReturn(List.of(item)); when(histories.selectList(any())).thenReturn(List.of());
|
||||
when(lines.lockCarton(3L)).thenReturn(List.of(stock));
|
||||
|
||||
var ownedCarton = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton(); ownedCarton.setCustomerId("2");
|
||||
when(cartons.selectById(3L)).thenReturn(ownedCarton);
|
||||
var rejected = assertThrows(ResponseStatusException.class, () -> guard.requireBound(app));
|
||||
assertTrue(rejected.getReason().contains("尚未绑定"));
|
||||
verify(cartons).selectById(3L);
|
||||
}
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings = {"TRUCK", "EXPRESS", "PICKUP", "AMAZON_PICKUP", "CUSTOMER_PICKUP"})
|
||||
void physicalBindingMustMatchLatestTargetAndCannotSurviveRework(String deliveryMethod) {
|
||||
var guard = new RelabelBindingGuard();
|
||||
var items = mock(RelabelItemMapper.class);
|
||||
var history = mock(PdaRelabelHistoryMapper.class);
|
||||
@@ -67,7 +167,7 @@ class RelabelBindingGuardTest {
|
||||
ReflectionTestUtils.setField(guard, "histories", history);
|
||||
ReflectionTestUtils.setField(guard, "positions", positions);
|
||||
ReflectionTestUtils.setField(guard, "objectMapper", new ObjectMapper());
|
||||
var app = new RelabelApplication(); app.setId(1L); app.setCustomerId("2");
|
||||
var app = new RelabelApplication(); app.setId(1L); app.setCustomerId("2"); app.setDeliveryMethod(deliveryMethod);
|
||||
var item = new RelabelItem(); item.setCartonMark("A"); item.setOperationType("CARTON_LABEL");
|
||||
item.setSourceInventoryPositionId(3L); item.setNotifiedPackageCount(1);
|
||||
when(items.selectList(any())).thenReturn(List.of(item));
|
||||
@@ -81,6 +181,9 @@ class RelabelBindingGuardTest {
|
||||
assertThrows(ResponseStatusException.class, () -> guard.requireBound(app));
|
||||
position.setPalletNo("NEW");
|
||||
assertDoesNotThrow(() -> guard.requireBound(app));
|
||||
position.setOverseasWarehouseId("OTHER");
|
||||
assertThrows(ResponseStatusException.class, () -> guard.requireBound(app));
|
||||
position.setOverseasWarehouseId(null);
|
||||
var rework = new PdaRelabelHistory(); rework.setAction("REWORK_ARCHIVE");
|
||||
when(history.selectList(any())).thenReturn(List.of(binding, rework));
|
||||
assertThrows(ResponseStatusException.class, () -> guard.requireBound(app));
|
||||
|
||||
+6
-2
@@ -16,7 +16,9 @@ import static org.mockito.Mockito.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
|
||||
class ReturnRelabelDeliveryTest {
|
||||
@Test void directNativeRelabelCreatesTwoPhysicalBoxesAndOneDeliveryPerDestination() {
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings = {"EXPRESS", "PICKUP", "AMAZON_PICKUP", "CUSTOMER_PICKUP"})
|
||||
void directNativeRelabelCreatesTwoPhysicalBoxesAndOneDeliveryPerDestination(String deliveryMethod) {
|
||||
var service = new RelabelApplicationService();
|
||||
var items = mock(RelabelItemMapper.class);
|
||||
var cartons = mock(ReturnInventoryCartonMapper.class);
|
||||
@@ -30,18 +32,20 @@ class ReturnRelabelDeliveryTest {
|
||||
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("EXPRESS");
|
||||
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)) {
|
||||
var carton = new ReturnInventoryCarton(); carton.setId(item.getReturnCartonId());
|
||||
carton.setCustomerId("C1"); carton.setRelabelApplicationId(1L); carton.setStatus("RELABEL_PENDING");
|
||||
carton.setOverseasWarehouseId("WH1");
|
||||
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));
|
||||
}
|
||||
|
||||
+68
-7
@@ -11,7 +11,10 @@ import com.baiyue.lastleg.returnreceipt.entity.*;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
@@ -36,9 +39,12 @@ class ReturnSkuConversionPersistenceTest {
|
||||
ReturnSkuConversionService service, target;
|
||||
ReturnInventoryBalanceMapper balances;
|
||||
DataSourceTransactionManager transactions;
|
||||
ReturnSkuMySqlTestDatabase mysql;
|
||||
|
||||
@BeforeEach void setUp() throws Exception {
|
||||
var ds = new DriverManagerDataSource("jdbc:h2:mem:sku-conversion-" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
@BeforeEach void setUp(TestInfo info) throws Exception {
|
||||
boolean mysqlMode = Boolean.getBoolean("returnSku.test.mysql");
|
||||
var ds = mysqlMode ? (mysql = ReturnSkuMySqlTestDatabase.open(info.getDisplayName())).dataSource()
|
||||
: new DriverManagerDataSource("jdbc:h2:mem:sku-conversion-" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
jdbc = new JdbcTemplate(ds); transactions = new DataSourceTransactionManager(ds);
|
||||
for (var entity : List.of(ReturnInventoryCarton.class, ReturnInventoryCartonItem.class, ReturnInventoryBalance.class,
|
||||
ReturnSkuConversion.class, ReturnInventoryOperation.class, RelabelItem.class)) {
|
||||
@@ -46,10 +52,12 @@ class ReturnSkuConversionPersistenceTest {
|
||||
.filter(f -> f.getAnnotation(com.baomidou.mybatisplus.annotation.TableField.class) == null
|
||||
|| f.getAnnotation(com.baomidou.mybatisplus.annotation.TableField.class).exist())
|
||||
.map(f -> f.getName().replaceAll("([A-Z])", "_$1").toLowerCase() + " "
|
||||
+ (f.getType() == String.class ? "VARCHAR(8000)" : f.getType() == LocalDateTime.class ? "TIMESTAMP"
|
||||
+ (f.getType() == String.class ? (mysqlMode ? (f.getName().endsWith("Json") ? "TEXT" : "VARCHAR(128)") : "VARCHAR(8000)") : f.getType() == LocalDateTime.class ? "TIMESTAMP"
|
||||
: f.getType() == java.math.BigDecimal.class ? "DECIMAL(20,6)" : "BIGINT")
|
||||
+ (f.getName().equals("id") ? " PRIMARY KEY" : "")).collect(Collectors.joining(","));
|
||||
jdbc.execute("CREATE TABLE " + entity.getAnnotation(TableName.class).value() + " (" + columns + ")");
|
||||
jdbc.execute("CREATE TABLE " + entity.getAnnotation(TableName.class).value() + " (" + columns + ")"
|
||||
+ (mysqlMode ? " ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE="
|
||||
+ (entity == ReturnInventoryCartonItem.class ? "utf8mb4_0900_ai_ci" : "utf8mb4_unicode_ci") : ""));
|
||||
}
|
||||
jdbc.execute("CREATE UNIQUE INDEX balance_sku ON ll_return_inventory_balance(customer_id,overseas_warehouse_id,sku_code)");
|
||||
jdbc.execute("CREATE UNIQUE INDEX conversion_round ON ll_return_sku_conversion(item_id,round_key,old_sku_code,new_sku_code)");
|
||||
@@ -74,6 +82,8 @@ class ReturnSkuConversionPersistenceTest {
|
||||
service = (ReturnSkuConversionService) proxy.getProxy();
|
||||
}
|
||||
|
||||
@AfterEach void cleanIsolatedMySql() throws Exception { if (mysql != null) mysql.close(); }
|
||||
|
||||
@ParameterizedTest @ValueSource(strings={"BOX", "PARCEL"})
|
||||
void partialConversionPreservesPhysicalUnitAndLocksUntilConfirmation(String type) {
|
||||
fixture(type); apply(1, 11, 8, "INITIAL");
|
||||
@@ -82,15 +92,66 @@ class ReturnSkuConversionPersistenceTest {
|
||||
assertEquals(2, count("SELECT sku_types FROM ll_return_inventory_carton WHERE id=101"));
|
||||
assertEquals(12, balance("A", "total_quantity")); assertEquals(8, balance("B", "total_quantity"));
|
||||
assertEquals("BOX".equals(type) ? 1 : 0, balance("B", "carton_quantity"));
|
||||
var locked = balances.page("C", "W", "B", 0, 20).get(0);
|
||||
assertEquals(0, locked.getAvailableQuantity()); assertEquals(8, locked.getRelabelLockedQuantity());
|
||||
// H2 verifies the persisted lock source and transaction, not MySQL's COLLATE projection.
|
||||
assertEquals("RELABEL_PENDING", jdbc.queryForObject("SELECT status FROM ll_return_inventory_carton WHERE id=101", String.class));
|
||||
assertEquals(1, count("SELECT relabel_application_id FROM ll_return_inventory_carton WHERE id=101"));
|
||||
assertEquals("APPLIED", jdbc.queryForObject("SELECT status FROM ll_return_sku_conversion", String.class));
|
||||
assertEquals(8, count("SELECT SUM(quantity) FROM ll_return_packing_trace WHERE target_carton_mark='UNIT-1' AND sku_code='B'"));
|
||||
service.confirm(app(1));
|
||||
jdbc.update("UPDATE ll_return_inventory_carton SET status='AVAILABLE',relabel_application_id=NULL WHERE id=101");
|
||||
assertEquals(8, balances.page("C", "W", "B", 0, 20).get(0).getAvailableQuantity());
|
||||
assertStock("B", 8, 8);
|
||||
assertEquals(8, balance("B", "available_quantity"));
|
||||
assertEquals("AVAILABLE", jdbc.queryForObject("SELECT status FROM ll_return_inventory_carton WHERE id=101", String.class));
|
||||
assertEquals("CONFIRMED", jdbc.queryForObject("SELECT status FROM ll_return_sku_conversion", String.class));
|
||||
}
|
||||
|
||||
@ParameterizedTest @ValueSource(strings={"BOX", "PARCEL"})
|
||||
@EnabledIfSystemProperty(named="returnSku.test.mysql", matches="true")
|
||||
void mysqlPageLocksConvertedSkuUntilConfirmationWithoutChangingPhysicalUnit(String type) {
|
||||
fixture(type);
|
||||
var physical = jdbc.queryForMap("SELECT id,carton_mark,source_return_no,source_unit_type,total_quantity FROM ll_return_inventory_carton WHERE id=101");
|
||||
apply(1,11,8,"INITIAL");
|
||||
assertStock("A",12,0); assertStock("B",8,8);
|
||||
assertEquals(12,balance("A","total_quantity")); assertEquals(8,balance("B","total_quantity"));
|
||||
assertEquals("BOX".equals(type) ? 1 : 0,balance("B","carton_quantity"));
|
||||
assertPageQuantity("B",0,8);
|
||||
service.confirm(app(1));
|
||||
jdbc.update("UPDATE ll_return_inventory_carton SET status='AVAILABLE',relabel_application_id=NULL WHERE id=101");
|
||||
assertPageQuantity("B",8,0);
|
||||
assertEquals(physical,jdbc.queryForMap("SELECT id,carton_mark,source_return_no,source_unit_type,total_quantity FROM ll_return_inventory_carton WHERE id=101"));
|
||||
mysql.record("physicalUnit",type); mysql.record("oldSkuQuantity",12); mysql.record("newSkuQuantity",8);
|
||||
mysql.record("beforeConfirmation",Map.of("available",0,"locked",8));
|
||||
mysql.record("afterConfirmation",Map.of("available",8,"locked",0));
|
||||
mysql.record("physicalUnitUnchanged",true);
|
||||
}
|
||||
|
||||
@ParameterizedTest @ValueSource(strings={"cafe", "CAFÉ"})
|
||||
@EnabledIfSystemProperty(named="returnSku.test.mysql", matches="true")
|
||||
void mysqlPageCollatesCaseAndAccentsWithoutJoiningOtherCustomerWarehouseOrSku(String stockSku) {
|
||||
fixture("BOX"); apply(1,11,8,"INITIAL");
|
||||
jdbc.update("UPDATE ll_return_inventory_balance SET sku_code='Café' WHERE sku_code='B'");
|
||||
jdbc.update("UPDATE ll_return_inventory_carton_item SET sku_code=? WHERE sku_code='B'",stockSku);
|
||||
for (int id=401;id<=403;id++) {
|
||||
String customer=id==401 ? "OTHER" : "C", warehouse=id==402 ? "OTHER" : "W", sku=id==403 ? "CAFETERIA" : "café";
|
||||
jdbc.update("INSERT INTO ll_return_inventory_carton(id,customer_id,overseas_warehouse_id,carton_mark,status,count_status,total_quantity,sku_types) VALUES(?,?,?,?,'RELABEL_PENDING','CONFIRMED',99,1)",id,customer,warehouse,"BOUNDARY-"+id);
|
||||
jdbc.update("INSERT INTO ll_return_inventory_carton_item(id,carton_id,customer_id,sku_code,quantity,relabelled_quantity,packing_reserved_quantity) VALUES(?,?,?,?,99,0,0)",id,id,customer,sku);
|
||||
}
|
||||
assertPageQuantity("CAFE",0,8);
|
||||
jdbc.update("UPDATE ll_return_inventory_carton SET status='AVAILABLE'");
|
||||
assertPageQuantity("café",8,0);
|
||||
mysql.record("stockSku",stockSku); mysql.record("balanceSku","Café");
|
||||
mysql.record("caseAndAccentJoin",true); mysql.record("foreignCustomerWarehouseAndDistinctSkuExcluded",true);
|
||||
}
|
||||
|
||||
void assertPageQuantity(String sku,long available,long locked) {
|
||||
var rows=balances.page("C","W",sku,0,20); assertEquals(1,rows.size());
|
||||
assertEquals(available,rows.get(0).getAvailableQuantity());
|
||||
assertEquals(locked,rows.get(0).getRelabelLockedQuantity());
|
||||
var platform=balances.platformPage("C","W",sku,0,20); assertEquals(1,platform.size());
|
||||
assertEquals(available,platform.get(0).getAvailableQuantity());
|
||||
assertEquals(locked,platform.get(0).getRelabelLockedQuantity());
|
||||
}
|
||||
|
||||
@Test void replayAndLaterPartialRoundDoNotConvertPriorPiecesTwice() {
|
||||
fixture("BOX"); apply(1, 11, 8, "INITIAL"); apply(1, 11, 8, "INITIAL");
|
||||
assertEquals(1, count("SELECT COUNT(*) FROM ll_return_sku_conversion")); assertStock("A", 12, 0);
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package com.baiyue.lastleg.returnreceipt.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import java.nio.file.*;
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
/** Opt-in TEST only. Each invocation owns a fresh, non-configurable schema; never connects to business schemas. */
|
||||
final class ReturnSkuMySqlTestDatabase implements AutoCloseable {
|
||||
private final String schema="cc_sku_accept_"+UUID.randomUUID().toString().replace("-","");
|
||||
private final Map<String,Object> evidence=new LinkedHashMap<>();
|
||||
private DriverManagerDataSource administration;
|
||||
private DataSource source;
|
||||
private boolean created;
|
||||
|
||||
static ReturnSkuMySqlTestDatabase open(String name) throws Exception {
|
||||
var database=new ReturnSkuMySqlTestDatabase();
|
||||
database.evidence.put("test",name); database.evidence.put("schema",database.schema);
|
||||
try { database.initialize(); return database; }
|
||||
catch(Exception failure) { database.close(); throw failure; }
|
||||
}
|
||||
|
||||
private void initialize() throws Exception {
|
||||
if(!Boolean.getBoolean("returnSku.test.mysql") || !"test".equals(required("CHONGCHONG_ENV")))
|
||||
throw new IllegalStateException("Explicit TEST MySQL opt-in required");
|
||||
if(!schema.matches("cc_sku_accept_[a-f0-9]{32}")) throw new IllegalStateException("Unsafe isolated schema");
|
||||
String host=required("CHONGCHONG_DB_HOST"),port=required("CHONGCHONG_DB_PORT");
|
||||
if(!host.matches("[A-Za-z0-9.-]+") || !port.matches("[0-9]{1,5}")) throw new IllegalStateException("Invalid TEST connection settings");
|
||||
String prefix="jdbc:mysql://"+host+":"+port+"/";
|
||||
String options="?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&connectTimeout=10000&socketTimeout=15000";
|
||||
String user=required("CHONGCHONG_DB_USERNAME"),password=required("CHONGCHONG_DB_PASSWORD");
|
||||
administration=new DriverManagerDataSource(prefix+"information_schema"+options,user,password);
|
||||
try(var connection=administration.getConnection();var statement=connection.createStatement()) {
|
||||
if(!"information_schema".equals(connection.getCatalog()) || !"MySQL".equals(connection.getMetaData().getDatabaseProductName())
|
||||
|| connection.getMetaData().getDatabaseMajorVersion()!=8) throw new IllegalStateException("Expected TEST MySQL 8 administrative read catalog");
|
||||
statement.execute("CREATE DATABASE `"+schema+"` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
created=true;
|
||||
} catch(SQLException failure) { throw new IllegalStateException("TEST isolated schema creation failed; SQLState="+failure.getSQLState()); }
|
||||
source=new DriverManagerDataSource(prefix+schema+options,user,password) {
|
||||
@Override public Connection getConnection() throws SQLException {
|
||||
var connection=super.getConnection();
|
||||
if(!created || !schema.equals(connection.getCatalog())) { connection.close(); throw new SQLException("Isolated schema boundary rejected"); }
|
||||
return connection;
|
||||
}
|
||||
};
|
||||
evidence.put("created",true); evidence.put("databaseProduct","MySQL 8");
|
||||
}
|
||||
|
||||
DataSource dataSource() { return source; }
|
||||
void record(String key,Object value) { evidence.put(key,value); }
|
||||
|
||||
@Override public void close() throws Exception {
|
||||
try {
|
||||
if(created) {
|
||||
if(!schema.matches("cc_sku_accept_[a-f0-9]{32}")) throw new IllegalStateException("Unsafe cleanup schema");
|
||||
try(var connection=administration.getConnection();var statement=connection.createStatement()) {
|
||||
if(!"information_schema".equals(connection.getCatalog())) throw new IllegalStateException("Unexpected cleanup catalog");
|
||||
statement.execute("DROP DATABASE `"+schema+"`"); created=false;
|
||||
try(var result=statement.executeQuery("SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name='"+schema+"'")) {
|
||||
result.next(); if(result.getInt(1)!=0) throw new IllegalStateException("Isolated schema cleanup not confirmed");
|
||||
}
|
||||
evidence.put("cleaned",true);
|
||||
} catch(SQLException failure) { throw new IllegalStateException("TEST isolated schema cleanup failed; SQLState="+failure.getSQLState()); }
|
||||
}
|
||||
} finally {
|
||||
var root=Path.of(System.getProperty("maven.multiModuleProjectDirectory","."));
|
||||
var directory=root.resolve("reports/return-sku-mysql-acceptance"); Files.createDirectories(directory);
|
||||
new ObjectMapper().writerWithDefaultPrettyPrinter().writeValue(directory.resolve(schema+".json").toFile(),evidence);
|
||||
}
|
||||
}
|
||||
|
||||
private static String required(String name) {
|
||||
String value=System.getenv(name);
|
||||
if(value==null || value.isBlank()) throw new IllegalStateException("Missing TEST environment variable "+name);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+16
@@ -56,6 +56,22 @@ class Phase5OverseasUpgradeSqlContractTest {
|
||||
assertThat(overseas).doesNotContain("\nUPDATE ", "\nDELETE ", "ALTER TABLE ll_");
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferFinalMeasurementColumnsAreCreatedBeforeAnyBusinessWrite() throws Exception {
|
||||
for (String path : List.of("lastleg-service/src/main/resources/db/lastleg.sql",
|
||||
"deploy/sql/海外区域统一增量-MySQL8.sql")) {
|
||||
String sql = repositoryFile(path);
|
||||
String carton = createStatement(sql, "ll_warehouse_transfer_carton");
|
||||
for (String column : List.of("customer_carton_mark", "length_cm", "width_cm", "height_cm",
|
||||
"gross_weight_kg", "counted_quantity", "count_idempotency_key", "count_request_hash",
|
||||
"counted_by", "counted_at", "discrepancy_reason", "transfer_pallet_id", "sku_items_json")) {
|
||||
assertThat(carton).as(path + ":" + column).contains(column);
|
||||
}
|
||||
assertThat(createStatement(sql, "ll_warehouse_transfer_item")).contains("declared_quantity");
|
||||
assertThat(createStatement(sql, "ll_cargo_discard_item")).contains("idx_ll_cargo_discard_return_carton");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void overseasFixedEntryAndModuleBaselineBothDefineCompletePhase5Tables() throws Exception {
|
||||
String baseline = repositoryFile("lastleg-service/src/main/resources/db/lastleg.sql");
|
||||
|
||||
+3
-2
@@ -26,7 +26,7 @@ class SchemaChineseCommentContractTest {
|
||||
private static final Pattern CHINESE = Pattern.compile("[\\u4e00-\\u9fff]");
|
||||
private static final Pattern TABLE = Pattern.compile(
|
||||
"(?is)CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(ll_[a-z0-9_]+)"
|
||||
+ "\\s*\\((.*?)\\)\\s*ENGINE\\s*=\\s*InnoDB[^;]*?COMMENT\\s*=\\s*'([^']*)'\\s*;");
|
||||
+ "\\s*\\((.*?)\\)\\s*ENGINE\\s*=\\s*InnoDB([^;]*);");
|
||||
|
||||
@Test
|
||||
void allLastlegTablesAndColumnsHaveChineseComments() throws Exception {
|
||||
@@ -104,7 +104,8 @@ class SchemaChineseCommentContractTest {
|
||||
Matcher matcher = TABLE.matcher(sql);
|
||||
while (matcher.find()) {
|
||||
String table = matcher.group(1);
|
||||
assertTrue(CHINESE.matcher(matcher.group(3)).find(), source + " 表备注必须包含中文: " + table);
|
||||
assertTrue(Pattern.compile("(?is)\\bCOMMENT\\s*=\\s*'[^']*[\\u4e00-\\u9fff][^']*'")
|
||||
.matcher(matcher.group(3)).find(), source + " 表备注必须包含中文: " + table);
|
||||
List<String> columns = splitTopLevel(matcher.group(2));
|
||||
assertFalse(columns.isEmpty(), source + " 表定义没有字段: " + table);
|
||||
List<String> columnNames = new ArrayList<>();
|
||||
|
||||
+52
-12
@@ -6,35 +6,62 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class SelfoperatedAnalyticsSchemaContractTest {
|
||||
private static final Pattern TABLE = Pattern.compile(
|
||||
"(?s)CREATE TABLE (ll_selfop_\\w+) \\(.*?\\) ENGINE=InnoDB[^;]+;");
|
||||
"(?s)CREATE TABLE (?:IF NOT EXISTS )?(ll_selfop_\\w+) \\(.*?\\) ENGINE=InnoDB[^;]+;");
|
||||
private static final Set<String> EXPECTED_TABLES = Set.of(
|
||||
"ll_selfop_outbound_discovery_scope", "ll_selfop_sync_scope", "ll_selfop_sync_task",
|
||||
"ll_selfop_sync_run", "ll_selfop_sync_stage", "ll_selfop_outbound_order",
|
||||
"ll_selfop_outbound_item", "ll_selfop_inventory_snapshot", "ll_selfop_sku_daily_sales",
|
||||
"ll_selfop_warehouse_daily_sales", "ll_selfop_sku_profile", "ll_selfop_owner",
|
||||
"ll_selfop_product", "ll_selfop_inventory_flow", "ll_selfop_inventory_day_version",
|
||||
"ll_selfop_inventory_daily");
|
||||
|
||||
private static Map<String, String> tables(String sql) {
|
||||
var result = new LinkedHashMap<String, String>();
|
||||
var matcher = TABLE.matcher(sql);
|
||||
var matcher = TABLE.matcher(sql.replace("\r\n", "\n"));
|
||||
while (matcher.find()) {
|
||||
assertNull(result.put(matcher.group(1), matcher.group()), "duplicate table definition");
|
||||
assertNull(result.put(matcher.group(1), matcher.group().replaceFirst("CREATE TABLE IF NOT EXISTS", "CREATE TABLE")),
|
||||
"duplicate table definition");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Test
|
||||
void sourceAndEmptyDatabaseBaselineContainIdenticalElevenIndependentTables() throws Exception {
|
||||
void sourceAndEmptyDatabaseBaselineContainAllCurrentIndependentTables() throws Exception {
|
||||
var source = tables(Files.readString(Path.of("src/main/resources/db/lastleg.sql")));
|
||||
var deployment = tables(Files.readString(Path.of("../deploy/sql/海外区域统一增量-MySQL8.sql")));
|
||||
assertEquals(11, source.size());
|
||||
assertEquals(EXPECTED_TABLES, source.keySet());
|
||||
assertEquals(source, deployment);
|
||||
for (var entry : source.entrySet()) {
|
||||
var ddl = entry.getValue();
|
||||
assertTrue(ddl.contains("COLLATE=utf8mb4_bin"), entry.getKey());
|
||||
assertTrue(ddl.contains("id BIGINT NOT NULL"));
|
||||
assertTrue(ddl.contains("created_at DATETIME(3) NOT NULL"));
|
||||
assertTrue(ddl.contains("updated_at DATETIME(3) NOT NULL"));
|
||||
boolean master = Set.of("ll_selfop_owner", "ll_selfop_product").contains(entry.getKey());
|
||||
boolean history = Set.of("ll_selfop_inventory_flow", "ll_selfop_inventory_day_version",
|
||||
"ll_selfop_inventory_daily").contains(entry.getKey());
|
||||
if (master) {
|
||||
assertTrue(ddl.contains("source_account VARCHAR(64) COLLATE utf8mb4_bin NOT NULL"));
|
||||
assertTrue(ddl.contains("customer_code VARCHAR(64) COLLATE utf8mb4_bin NOT NULL"));
|
||||
assertTrue(ddl.contains("observed_at_utc DATETIME(6) NOT NULL"));
|
||||
} else {
|
||||
assertTrue(ddl.contains("COLLATE=utf8mb4_bin"), entry.getKey());
|
||||
}
|
||||
if (!master && !history) {
|
||||
assertTrue(ddl.contains("created_at DATETIME(3) NOT NULL"), entry.getKey());
|
||||
assertTrue(ddl.contains("updated_at DATETIME(3) NOT NULL"), entry.getKey());
|
||||
}
|
||||
assertTrue(ddl.contains("PRIMARY KEY ("), entry.getKey());
|
||||
if (entry.getKey().equals("ll_selfop_inventory_daily")) {
|
||||
assertTrue(ddl.contains("PRIMARY KEY (day_version_id,customer_code,sku_code,product_quality)"));
|
||||
} else if (entry.getKey().equals("ll_selfop_warehouse_daily_sales")) {
|
||||
assertTrue(ddl.contains("PRIMARY KEY (sales_run_id,source_account,warehouse_code,business_date)"));
|
||||
} else {
|
||||
assertTrue(ddl.contains("\n id BIGINT NOT NULL"), entry.getKey());
|
||||
}
|
||||
assertFalse(ddl.contains("AUTO_INCREMENT"));
|
||||
assertFalse(ddl.contains("tenant_id"));
|
||||
assertFalse(ddl.contains("FOREIGN KEY"));
|
||||
@@ -107,21 +134,34 @@ class SelfoperatedAnalyticsSchemaContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAndSyncPermissionsAreIndependentWithThreeAuthorizedAdminMenus() throws Exception {
|
||||
void readSyncAndExportPermissionsAreIndependentWithFourAuthorizedAdminMenus() throws Exception {
|
||||
var seed = Files.readString(Path.of("../iam/src/main/resources/db/selfoperated-analytics-permissions.sql"));
|
||||
var unified = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"));
|
||||
assertTrue(unified.contains(seed.strip()));
|
||||
// Compare executable seed statements; explanatory comments and line endings are not SQL facts.
|
||||
String seedSql = seed.replace("\r\n", "\n").replaceAll("(?m)^\\s*--[^\n]*", "");
|
||||
String unifiedSql = unified.replace("\r\n", "\n").replaceAll("(?m)^\\s*--[^\n]*", "");
|
||||
int comparedStatements = 0;
|
||||
for (String statement : seedSql.split(";")) {
|
||||
if (statement.strip().startsWith("INSERT INTO")) {
|
||||
assertTrue(unifiedSql.contains(statement.strip() + ";"), statement);
|
||||
comparedStatements++;
|
||||
}
|
||||
}
|
||||
assertEquals(2, comparedStatements, "permission and role grant INSERT statements must both be checked");
|
||||
assertTrue(seed.contains("platform:self-operated-analytics:list"));
|
||||
assertTrue(seed.contains("platform:self-operated-analytics:sync"));
|
||||
for (var route : new String[]{"/self-operated/sales", "/self-operated/inventory", "/self-operated/collection"}) {
|
||||
assertTrue(seed.contains("platform:self-operated-analytics:export"));
|
||||
for (var route : new String[]{"/self-operated/overview", "/self-operated/sales", "/self-operated/inventory", "/self-operated/collection"}) {
|
||||
assertTrue(seed.contains(route));
|
||||
}
|
||||
assertTrue(seed.contains("'API',5118100,'platform:self-operated-analytics:list'"));
|
||||
assertTrue(seed.contains("'API',5118130,'platform:self-operated-analytics:sync'"));
|
||||
assertTrue(seed.contains("'API',5118120,'platform:self-operated-analytics:export'"));
|
||||
assertFalse(seed.contains("'MERCHANT'"));
|
||||
assertFalse(seed.contains("INSERT IGNORE"));
|
||||
assertFalse(seed.contains("ON DUPLICATE KEY"));
|
||||
assertTrue(seed.contains("(4000001,5118000,0,"));
|
||||
assertTrue(seed.contains("(4000001,5118001,0,"));
|
||||
assertTrue(seed.contains("(4000001,5118002,0,"));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -217,6 +217,7 @@ class WarehouseTransferRelabelPersistenceTest {
|
||||
ReflectionTestUtils.setField(service,"pdaTasks",tasks);
|
||||
ReflectionTestUtils.setField(service,"returnOrders",mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper.class));
|
||||
ReflectionTestUtils.setField(service,"skuConversions",mock(com.baiyue.lastleg.relabel.service.ReturnSkuConversionService.class));
|
||||
ReflectionTestUtils.setField(service,"bindingGuard",mock(com.baiyue.lastleg.relabel.service.RelabelBindingGuard.class));
|
||||
ReflectionTestUtils.setField(service,"histories",mock(com.baiyue.lastleg.relabel.mapper.RelabelStatusHistoryMapper.class));
|
||||
ReflectionTestUtils.setField(service,"orderHistories",mock(com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper.class));
|
||||
var orders=mock(com.baiyue.lastleg.mapper.LastlegOrderMapper.class);var order=new com.baiyue.lastleg.entity.LastlegOrder();
|
||||
|
||||
Reference in New Issue
Block a user