提交
This commit is contained in:
+33
@@ -0,0 +1,33 @@
|
||||
package com.baiyue.lastleg.delivery;
|
||||
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundOrder;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundPalletItem;
|
||||
import com.baiyue.lastleg.delivery.mapper.OutboundPalletItemMapper;
|
||||
import com.baiyue.lastleg.delivery.repository.OutboundQuantityRepository;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class OutboundQuantityRepositoryTest {
|
||||
/** 箱唛事实优先于历史件数,同箱多 SKU、多箱一行和无箱唛库存均须正确计数。 */
|
||||
@Test void countsUniqueMarksWithoutDependingOnHistoricPieceMetadata() {
|
||||
var mapper = mock(OutboundPalletItemMapper.class);
|
||||
var repository = new OutboundQuantityRepository();
|
||||
ReflectionTestUtils.setField(repository, "pallets", mapper);
|
||||
var outbound = new OutboundOrder();
|
||||
outbound.setId(1L); outbound.setCustomerId("C-1"); outbound.setOutboundType("DISCARD_OUTBOUND");
|
||||
outbound.setPackageCount(99);
|
||||
var a = new OutboundPalletItem(); a.setCartonMarks("BOX-1, BOX-2"); a.setPackageCount(5);
|
||||
var b = new OutboundPalletItem(); b.setCartonMarks("BOX-2,BOX-1"); b.setPackageCount(8);
|
||||
var unmarked = new OutboundPalletItem(); unmarked.setPackageCount(3);
|
||||
when(mapper.findByOutbound("C-1", 1L)).thenReturn(List.of(a, b, unmarked));
|
||||
assertEquals(5, repository.boxes(outbound));
|
||||
assertEquals(99, outbound.getPackageCount());
|
||||
outbound.setOutboundType("RETURN_OUTBOUND");
|
||||
assertEquals(99, repository.boxes(outbound));
|
||||
verify(mapper, times(1)).findByOutbound("C-1", 1L);
|
||||
verifyNoMoreInteractions(mapper);
|
||||
}
|
||||
}
|
||||
+24
@@ -133,6 +133,9 @@ class DeliveryTransferServiceTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new DeliveryTransferService();
|
||||
var quantities = new com.baiyue.lastleg.delivery.repository.OutboundQuantityRepository();
|
||||
ReflectionTestUtils.setField(quantities, "pallets", outboundPallets);
|
||||
ReflectionTestUtils.setField(service, "outboundQuantities", quantities);
|
||||
ReflectionTestUtils.setField(service, "deliveryExecution", new DeliveryExecutionGuard(new com.baiyue.lastleg.delivery.repository.DeliveryExecutionRepository(outboundOrders, deliveryOrders)));
|
||||
ReflectionTestUtils.setField(service, "execution", execution);
|
||||
ReflectionTestUtils.setField(service,"cancellations",new com.baiyue.lastleg.delivery.repository.DeliveryCancellationRepository(
|
||||
@@ -491,6 +494,27 @@ class DeliveryTransferServiceTest {
|
||||
assertEquals("DISCARD_OUTBOUND".equals(type) ? null : "LX-REAL-001", detail.outbound().lxCodes());
|
||||
}
|
||||
|
||||
/** 历史废弃单一箱内的多种 SKU 件数不能累加成出库箱数。 */
|
||||
@Test
|
||||
void discardListAndDetailCountPhysicalCartonsInsteadOfHistoricPieces() {
|
||||
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "discard-301");
|
||||
outbound.setOutboundType("DISCARD_OUTBOUND"); outbound.setPackageCount(5);
|
||||
var a = pallet(501L, "P-1", null, null, "RT-1", 2);
|
||||
var b = pallet(502L, "P-1", null, null, "RT-1", 3);
|
||||
a.setReturnSkuQuantity(2L); b.setReturnSkuQuantity(3L);
|
||||
a.setCartonMarks("BOX-1"); b.setCartonMarks("BOX-1");
|
||||
when(outboundOrders.findGlobalByNo("OUT-301")).thenReturn(outbound);
|
||||
when(outboundPallets.findByOutbound("TENANT-A", 301L)).thenReturn(List.of(a, b));
|
||||
when(outboundOrders.selectPage(any(Page.class), any())).thenReturn(new Page<OutboundOrder>(1, 10, 1).setRecords(List.of(outbound)));
|
||||
|
||||
assertEquals(1, service.outboundDetail("OUT-301").outbound().packageCount());
|
||||
var query = new DeliveryRequests.Query(1, 10, "OUT-301", null, null, null, null, null, null, null, null, null, null, "DISCARD_OUTBOUND");
|
||||
assertEquals(1, service.outboundPage(null, query).records().get(0).packageCount());
|
||||
assertEquals(5, outbound.getPackageCount());
|
||||
assertEquals(2L, a.getReturnSkuQuantity()); assertEquals(3L, b.getReturnSkuQuantity());
|
||||
verify(outboundOrders, never()).updateById(any(OutboundOrder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dispatchOutboundStillRequiresItsDeliveryDocument() {
|
||||
OutboundOrder outbound = outbound(301L, 101L, "DSP-101", "OUT-301", "dispatch-301");
|
||||
|
||||
+5
@@ -57,6 +57,11 @@ class ReturnOutboundClosureTest {
|
||||
ReturnInventoryCarton carton;
|
||||
|
||||
@BeforeEach void setup() {
|
||||
// 取消入口先查询幂等记录,手动组装的测试服务也需要注入该仓储。
|
||||
ReflectionTestUtils.setField(service, "cancellations", new com.baiyue.lastleg.delivery.repository.DeliveryCancellationRepository(
|
||||
outbounds, items, deliveries, tasks, mock(DeliveryOperationMapper.class), relabelApplications, cartons,
|
||||
mock(com.baiyue.lastleg.transfer.mapper.WarehouseTransferOrderMapper.class),
|
||||
mock(com.baiyue.lastleg.mapper.LastlegOrderMapper.class), mock(com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper.class)));
|
||||
ReflectionTestUtils.setField(service, "outboundOrders", outbounds);
|
||||
ReflectionTestUtils.setField(service, "outboundPallets", items);
|
||||
ReflectionTestUtils.setField(service, "pdaTasks", tasks);
|
||||
|
||||
+2
@@ -54,6 +54,8 @@ class ReturnOutboundPdaPersistenceTest {
|
||||
+ (field.getName().equals("id") ? " PRIMARY KEY" : "")).collect(Collectors.joining(","));
|
||||
jdbc.execute("CREATE TABLE " + entity.getAnnotation(TableName.class).value() + " (" + columns + ")");
|
||||
}
|
||||
jdbc.execute("CREATE TABLE ll_cargo_discard_application(id BIGINT,customer_id VARCHAR(2000),overseas_warehouse_id VARCHAR(2000),discard_no VARCHAR(2000))");
|
||||
jdbc.execute("CREATE TABLE ll_cargo_discard_item(id BIGINT,application_id BIGINT,customer_id VARCHAR(2000),return_carton_id BIGINT,sku_code VARCHAR(2000),product_name VARCHAR(2000),pda_status VARCHAR(2000),available_packages_snapshot BIGINT,actual_discard_packages BIGINT,pda_operator_id VARCHAR(2000),pda_completed_at TIMESTAMP)");
|
||||
var configuration = new MybatisConfiguration(); configuration.setMapUnderscoreToCamelCase(true);
|
||||
for (Class<?> mapper : List.of(OutboundOrderMapper.class, OutboundPalletItemMapper.class,
|
||||
OutboundPdaTaskMapper.class, OutboundPdaPickMapper.class, DeliveryOrderMapper.class, ReturnInventoryCartonMapper.class,
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.lastleg.discard;
|
||||
|
||||
import com.baiyue.lastleg.discard.model.CargoDiscardReturnMark;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/** 废弃标签与签收实物标签一致,箱及托盘来源仍使用独立箱唛。 */
|
||||
class CargoDiscardReturnMarkTest {
|
||||
/** 兼容历史 RP 库存记录,不通过字符串替换猜测 PK 编码。 */
|
||||
@Test void historicalParcelUsesExactReceiptUnitCode() {
|
||||
var carton = new ReturnInventoryCarton(); carton.setSourceUnitType("PARCEL");
|
||||
carton.setCartonMark("RP-G1OTH7E5RRB6"); carton.setSourceUnitCode("PK-G1OTH7E5RRB5");
|
||||
assertEquals("PK-G1OTH7E5RRB5", CargoDiscardReturnMark.of(carton));
|
||||
carton.setSourceUnitType("BOX"); assertEquals("RP-G1OTH7E5RRB6", CargoDiscardReturnMark.of(carton));
|
||||
carton.setSourceUnitType("PARCEL"); carton.setSourceUnitCode(" ");
|
||||
assertEquals("RP-G1OTH7E5RRB6", CargoDiscardReturnMark.of(carton));
|
||||
assertNull(CargoDiscardReturnMark.of(null));
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -20,8 +20,7 @@ class ReturnDiscardMovementMapperTest {
|
||||
var source = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
var db = new JdbcTemplate(source);
|
||||
db.execute("CREATE TABLE ll_cargo_discard_application(id BIGINT,customer_id VARCHAR(20),overseas_warehouse_id VARCHAR(20),discard_no VARCHAR(30))");
|
||||
db.execute("CREATE TABLE ll_return_inventory_carton(id BIGINT,customer_id VARCHAR(20),overseas_warehouse_id VARCHAR(20))");
|
||||
db.execute("ALTER TABLE ll_return_inventory_carton ADD carton_mark VARCHAR(30), ADD source_batch_no VARCHAR(30), ADD created_at TIMESTAMP");
|
||||
db.execute("CREATE TABLE ll_return_inventory_carton(id BIGINT,customer_id VARCHAR(20),overseas_warehouse_id VARCHAR(20),carton_mark VARCHAR(30),source_batch_no VARCHAR(30),created_at TIMESTAMP)");
|
||||
db.execute("CREATE TABLE ll_return_inventory_carton_item(carton_id BIGINT,sku_code VARCHAR(30),product_name_zh VARCHAR(30),quantity BIGINT)");
|
||||
db.execute("CREATE TABLE ll_return_receipt(id BIGINT,return_order_id BIGINT,receipt_no VARCHAR(30),signed_at TIMESTAMP)");
|
||||
db.execute("CREATE TABLE ll_return_order(id BIGINT,customer_id VARCHAR(20),overseas_warehouse_id VARCHAR(20))");
|
||||
|
||||
+4
-3
@@ -193,18 +193,19 @@ class PdaCargoDiscardServiceTest {
|
||||
@Test void returnCartonUsesPieceUnitAndRequiresSelectedCartonEvenWhenBoundToPallet() {
|
||||
item.setSourceType("RETURN_CARTON"); item.setReturnCartonId(31L); item.setDiscardPackages(5);
|
||||
var carton = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton();
|
||||
carton.setCustomerId("CUSTOMER"); carton.setPalletCode("PALLET-1"); carton.setCartonMark("RETURN-BOX-1");
|
||||
carton.setCustomerId("CUSTOMER"); carton.setPalletCode("PALLET-1"); carton.setCartonMark("RP-OLD");
|
||||
carton.setSourceUnitType("PARCEL"); carton.setSourceUnitCode("PK-REAL");
|
||||
when(returnCartons.selectById(31L)).thenReturn(carton);
|
||||
when(applications.selectList(any())).thenReturn(List.of(application));
|
||||
var task = service.resolve(identity, "PALLET-1", "CD-1");
|
||||
assertEquals("CARTON", task.scanMode());
|
||||
assertEquals(List.of("RETURN-BOX-1"), task.cartonMarks());
|
||||
assertEquals(List.of("PK-REAL"), task.cartonMarks());
|
||||
assertEquals(5, task.skus().get(0).plannedPackages());
|
||||
assertEquals("件", task.skus().get(0).quantityUnit());
|
||||
assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", request(5)));
|
||||
verifyNoInteractions(adjustments);
|
||||
var confirm = new PdaCargoDiscardDtos.Confirm(List.of(new PdaCargoDiscardDtos.ActualLine(11L, 5)),
|
||||
List.of(101L), List.of("RETURN-BOX-1"), false);
|
||||
List.of(101L), List.of("PK-REAL"), false);
|
||||
service.confirm(identity, "CD-1", "PALLET-1", confirm);
|
||||
assertEquals(1, application.getTotalPackages());
|
||||
assertEquals(new BigDecimal("5.00"), application.getTotalFee());
|
||||
|
||||
+3
-2
@@ -94,9 +94,10 @@ class CargoDiscardOutboundServiceTest {
|
||||
@Test void returnSourceKeepsRealCartonMarkWithoutInventingPalletNumber() {
|
||||
first.setReturnCartonId(31L); first.setSourceType("RETURN_CARTON");
|
||||
var carton = new ReturnInventoryCarton(); carton.setId(31L); carton.setCustomerId("C1");
|
||||
carton.setOverseasWarehouseId("99"); carton.setCartonMark("BOX-31");
|
||||
carton.setOverseasWarehouseId("99"); carton.setCartonMark("RP-OLD");
|
||||
carton.setSourceUnitType("PARCEL"); carton.setSourceUnitCode("PK-REAL");
|
||||
when(returnCartons.selectById(31L)).thenReturn(carton);
|
||||
service.create(application, "OP"); assertNull(snapshots.get(0).getPalletNo()); assertEquals("BOX-31", snapshots.get(0).getCartonMarks());
|
||||
service.create(application, "OP"); assertNull(snapshots.get(0).getPalletNo()); assertEquals("PK-REAL", snapshots.get(0).getCartonMarks());
|
||||
assertEquals(3L, snapshots.get(0).getReturnSkuQuantity());
|
||||
}
|
||||
@Test void invalidSourcePreventsAnyOutboundInsert() {
|
||||
|
||||
+20
@@ -47,6 +47,26 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CargoDiscardServiceTest {
|
||||
/** 历史包裹候选必须沿用签收 PK 码,不能展示内部 RP 码。 */
|
||||
@Test
|
||||
void parcelCandidatesAndDetailsUseReceiptLabel() {
|
||||
var carton = new ReturnInventoryCarton(); carton.setId(901L); carton.setCustomerId("100");
|
||||
carton.setOverseasWarehouseId("99"); carton.setSourceReturnNo("TT-BYWL-261019180631");
|
||||
carton.setSourceUnitType("PARCEL"); carton.setSourceUnitCode("PK-G1OTH7E5RRB5");
|
||||
carton.setCartonMark("RP-G1OTH7E5RRB6"); carton.setStatus("AVAILABLE");
|
||||
var sku = new ReturnInventoryCartonItem(); sku.setSkuCode("SKU"); sku.setQuantity(6L);
|
||||
when(returnCartons.findAvailable("100", null)).thenReturn(List.of(carton));
|
||||
when(returnCartons.selectById(901L)).thenReturn(carton);
|
||||
when(returnCartonItems.listCarton(901L)).thenReturn(List.of(sku));
|
||||
var candidates = service.candidates("100", "PK-G1OTH7E5RRB5");
|
||||
assertEquals(1, candidates.size()); assertEquals("PK-G1OTH7E5RRB5", candidates.get(0).cartonMark());
|
||||
assertEquals("PK-G1OTH7E5RRB5", candidates.get(0).cartons().get(0).cartonMark());
|
||||
storedApplication.set(pendingApplication());
|
||||
var line = item(); line.setSourceType("RETURN_CARTON"); line.setReturnCartonId(901L); storedItems.add(line);
|
||||
var detail = service.detail("100", "FH-1");
|
||||
assertEquals(List.of("PK-G1OTH7E5RRB5"), detail.items().get(0).cartonMarks());
|
||||
}
|
||||
|
||||
@Test
|
||||
void customerConfirmationClosesPdaCompletedApplicationOnceWithoutAnotherDeduction() {
|
||||
CargoDiscardApplication application = new CargoDiscardApplication();
|
||||
|
||||
+8
-4
@@ -112,20 +112,24 @@ class CargoDiscardStockServiceTest {
|
||||
verifyNoInteractions(commands, items, links);
|
||||
}
|
||||
|
||||
@Test void singlePositionTransferBoxesReleaseBackToTheirOriginalExpectedStatus() {
|
||||
@Test void receivedTransferBoxesAreSelectableAndReleaseBackToCountedStatus() {
|
||||
position.setAvailablePackages(2L); balance.setAvailablePackages(2L);
|
||||
var first = carton(1L, "WT-1#2-1"); first.setSourceType("TRANSFER");
|
||||
var second = carton(2L, "WT-1#2-2"); second.setSourceType("TRANSFER");
|
||||
when(stock.genericCartons("C1", 42L)).thenReturn(List.of(first, second));
|
||||
when(stock.changeTransferCartonStatus("C1", 2L, "EXPECTED", "DISCARD_PENDING")).thenReturn(1);
|
||||
when(stock.genericPositions("C1", "WT")).thenReturn(List.of(position));
|
||||
var candidate = service.candidates("C1", "WT").get(0);
|
||||
assertTrue(candidate.cartonSelectionSupported());
|
||||
assertEquals(List.of("WT-1#2-1", "WT-1#2-2"), candidate.cartons().stream().map(box -> box.cartonMark()).toList());
|
||||
when(stock.changeTransferCartonStatus("C1", 2L, "COUNTED", "DISCARD_PENDING")).thenReturn(1);
|
||||
service.reserve("C1", "OP", application, new CargoDiscardRequests.Item(null, null, "SKU", 1, 42L, List.of(2L)), LocalDateTime.now());
|
||||
var capture = ArgumentCaptor.forClass(CargoDiscardCarton.class); verify(links).insert(capture.capture());
|
||||
assertEquals("TRANSFER", capture.getValue().getSourceType());
|
||||
when(links.listApplication("C1", 11L)).thenReturn(List.of(capture.getValue()));
|
||||
when(stock.changeTransferCartonStatus("C1", 2L, "DISCARD_PENDING", "EXPECTED")).thenReturn(1);
|
||||
when(stock.changeTransferCartonStatus("C1", 2L, "DISCARD_PENDING", "COUNTED")).thenReturn(1);
|
||||
var line = ArgumentCaptor.forClass(CargoDiscardItem.class); verify(items).insert(line.capture());
|
||||
service.release("C1", application, line.getValue());
|
||||
verify(stock).changeTransferCartonStatus("C1", 2L, "DISCARD_PENDING", "EXPECTED");
|
||||
verify(stock).changeTransferCartonStatus("C1", 2L, "DISCARD_PENDING", "COUNTED");
|
||||
verify(stock, never()).changeGenericCartonStatus(anyString(), anyLong(), anyString(), anyString());
|
||||
}
|
||||
|
||||
|
||||
+13
@@ -10,6 +10,19 @@ import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class ProcurementRequestBoundaryTest {
|
||||
@Test void purchaseCategoryMayBeOmittedOrBlankButCannotExceedLimit() throws Exception {
|
||||
var json = new ObjectMapper();
|
||||
try (var factory = jakarta.validation.Validation.buildDefaultValidatorFactory()) {
|
||||
var validator = factory.getValidator();
|
||||
for (String category : List.of("null", "\"\"", "\"sample\"")) {
|
||||
var request = json.readValue("{\"purchaseCategory\":" + category + "}", OverseasProcurementRequests.Create.class);
|
||||
assertThat(validator.validate(request)).noneMatch(v -> "purchaseCategory".equals(v.getPropertyPath().toString()));
|
||||
}
|
||||
var oversized = json.readValue("{\"purchaseCategory\":\"" + "x".repeat(129) + "\"}", OverseasProcurementRequests.Create.class);
|
||||
assertThat(validator.validate(oversized)).anyMatch(v -> "purchaseCategory".equals(v.getPropertyPath().toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test void createRejectsMultiProductAndBatchExtensionsWithApplicationJacksonDefaults() {
|
||||
new org.springframework.boot.test.context.runner.ApplicationContextRunner()
|
||||
.withConfiguration(org.springframework.boot.autoconfigure.AutoConfigurations.of(
|
||||
|
||||
+1
-1
@@ -185,7 +185,7 @@ class ReturnInventoryServiceTest {
|
||||
var receipt = new ReturnReceipt(); receipt.setId(21L); receipt.setUnitType("PARCEL"); receipt.setUnitCode("PK-1");
|
||||
var source = service.createReceiptCarton(completedOrder(), receipt, null,
|
||||
List.of(new ReturnInventoryService.ReceiptLine("A", "商品A", 3)));
|
||||
assertThat(source.getCartonMark()).startsWith("RP-").hasSizeLessThanOrEqualTo(16);
|
||||
assertThat(source.getCartonMark()).isEqualTo("PK-1");
|
||||
assertThat(source.getSourceUnitType()).isEqualTo("PARCEL");
|
||||
verifyNoInteractions(numbers);
|
||||
assertThat(source.getLabelStatus()).isEqualTo("CONFIRMED");
|
||||
|
||||
+13
@@ -56,6 +56,16 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ReturnReceiptServiceTest {
|
||||
@Test void rejectsUnprintablePhysicalCodesBeforeReceiptWrites() {
|
||||
for (String code : List.of("中文包裹", "PK-中文")) {
|
||||
var request = new PdaReturnReceiptRequests.Confirm("RT-1", null, "RETURN_NO", null,
|
||||
List.of(new PdaReturnReceiptRequests.Item("SKU1", 1)), "INVALID-CODE", "PARCEL", code,
|
||||
java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, null, true);
|
||||
assertThat(assertThrows(ResponseStatusException.class, () -> service.confirm(identity, request)).getReason())
|
||||
.contains("实物编码无法生成条码");
|
||||
}
|
||||
verifyNoInteractions(orders, receipts, orderItems, receiptItems, inventory);
|
||||
}
|
||||
@Test void countedCargoReplacesForecastTotalsAndRepeatedSyncUpdatesTheSameDetail() {
|
||||
var details = org.mockito.Mockito.mock(com.baiyue.lastleg.mapper.LastlegOrderDetailMapper.class);
|
||||
ReflectionTestUtils.setField(service, "lastlegOrderDetails", details);
|
||||
@@ -268,6 +278,8 @@ class ReturnReceiptServiceTest {
|
||||
assertEquals("RL-MULTI", order.getRelabelNo());
|
||||
var application = ArgumentCaptor.forClass(RelabelApplication.class);
|
||||
verify(relabelApplications).insert(application.capture());
|
||||
assertEquals("", application.getValue().getContactName());
|
||||
assertEquals("", application.getValue().getContactPhone());
|
||||
assertEquals(firstQuantity + secondQuantity, application.getValue().getNotifiedPackageCount().longValue());
|
||||
assertEquals(2, application.getValue().getItemCount());
|
||||
var lines = ArgumentCaptor.forClass(RelabelItem.class);
|
||||
@@ -637,3 +649,4 @@ class ReturnReceiptServiceTest {
|
||||
"BOX", palletCode, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, 1L, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-3
@@ -36,6 +36,16 @@ class TransferInventorySourceTest {
|
||||
db.execute("CREATE TABLE ll_relabel_application(id BIGINT,customer_id VARCHAR,overseas_warehouse_id VARCHAR,status VARCHAR,customer_confirmed_at TIMESTAMP)");
|
||||
db.execute("CREATE TABLE ll_relabel_item(application_id BIGINT,customer_id VARCHAR,source_inbound_code VARCHAR,source_carton_mark VARCHAR,carton_mark VARCHAR)");
|
||||
db.execute("CREATE TABLE ll_relabel_pda_unit_fact(item_id BIGINT,result VARCHAR)");
|
||||
db.execute("CREATE TABLE ll_dropship_carton(id BIGINT,customer_id VARCHAR,order_id BIGINT,profile_id BIGINT,carton_mark VARCHAR,sku_code VARCHAR,product_quantity BIGINT,status VARCHAR)");
|
||||
db.execute("CREATE TABLE ll_dropship_carton_sku(customer_id VARCHAR,carton_id BIGINT,sku_code VARCHAR,product_quantity BIGINT)");
|
||||
db.execute("CREATE TABLE ll_dropship_order_profile(id BIGINT,customer_id VARCHAR,inventory_position_id BIGINT,status VARCHAR)");
|
||||
db.execute("CREATE TABLE ll_inventory_position(id BIGINT,customer_id VARCHAR,balance_id BIGINT,available_packages BIGINT)");
|
||||
db.execute("CREATE TABLE ll_inventory_balance(id BIGINT,customer_id VARCHAR,available_packages BIGINT)");
|
||||
db.update("INSERT INTO ll_dropship_order_profile VALUES(101,'A',201,'ACTIVE')");
|
||||
db.update("INSERT INTO ll_inventory_position VALUES(201,'A',301,2)");
|
||||
db.update("INSERT INTO ll_inventory_balance VALUES(301,'A',2)");
|
||||
db.update("INSERT INTO ll_dropship_carton VALUES(401,'A',1,101,'DROPSHIP','SKU',5,'AVAILABLE'),(402,'A',1,101,'SHIPPED','SKU',5,'OUTBOUNDED')");
|
||||
db.update("INSERT INTO ll_dropship_carton_sku VALUES('A',401,'SKU',3),('A',401,'SKU-B',2)");
|
||||
db.update("INSERT INTO ll_order VALUES(1,'A','IN','WH'),(2,'B','IN','WH'),(3,'A','IN','OTHER'),(4,'A','OTHER','WH')");
|
||||
db.update("INSERT INTO ll_inbound_plan_carton VALUES('A',1,'FIRST','S','ONT8','Amazon'),('A',1,'RELABELED','S','ONT8','Amazon'),('B',2,'FOREIGN','S','ONT8','Amazon'),('A',3,'WRONG-WH','S','ONT8','Amazon'),('A',4,'WRONG-IN','S','ONT8','Amazon'),('B',1,'FORGED','S','ONT8','Amazon')");
|
||||
db.update("INSERT INTO ll_relabel_application VALUES(51,'A','WH','COMPLETED',CURRENT_TIMESTAMP()),(52,'A','WH','PROCESSING',NULL),(53,'A','WH','CANCELLED',NULL),(54,'A','WH','REJECTED',NULL),(55,'B','WH','COMPLETED',CURRENT_TIMESTAMP()),(56,'A','OTHER','COMPLETED',CURRENT_TIMESTAMP())");
|
||||
@@ -47,9 +57,13 @@ class TransferInventorySourceTest {
|
||||
var config=new Configuration(new Environment("test",new JdbcTransactionFactory(),ds)); config.addMapper(InboundSnapshotMapper.class);
|
||||
try(var session=new SqlSessionFactoryBuilder().build(config).openSession()) {
|
||||
var result=session.getMapper(InboundSnapshotMapper.class).sourceCartons("A","WH","IN");
|
||||
assertEquals(List.of("FIRST", "MEASURED"),result.stream().map(InboundSnapshotMapper.SourceCarton::cartonMark).toList());
|
||||
assertEquals("ONT8",result.get(0).destinationWarehouseCode());
|
||||
assertNull(result.get(0).quantity()); assertEquals(20L,result.get(1).quantity());
|
||||
assertEquals(List.of("DROPSHIP", "FIRST", "MEASURED"),result.stream().map(InboundSnapshotMapper.SourceCarton::cartonMark).toList());
|
||||
assertEquals(5L,result.get(0).quantity()); assertTrue(result.get(0).skuItemsJson().contains("SKU"));
|
||||
assertTrue(result.get(0).skuItemsJson().contains("SKU-B"), "混装箱内所有 SKU 均须保留,箱子只返回一次");
|
||||
assertEquals("ONT8",result.get(1).destinationWarehouseCode());
|
||||
assertNull(result.get(1).quantity()); assertEquals(20L,result.get(2).quantity());
|
||||
db.update("UPDATE ll_inventory_position SET available_packages=0"); session.clearCache();
|
||||
assertEquals(List.of("FIRST", "MEASURED"),session.getMapper(InboundSnapshotMapper.class).sourceCartons("A","WH","IN").stream().map(InboundSnapshotMapper.SourceCarton::cartonMark).toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -175,6 +175,18 @@ class WarehouseTransferCartonCountTest {
|
||||
ia.setDeclaredQuantity(4L);
|
||||
assertTrue(service.hasSkuReceiptDifference(List.of(a),List.of(ia,ib)));
|
||||
}
|
||||
/** 新箱必须先补客户预报,PDA不能绕过预报生成另一套箱号。 */
|
||||
@Test void missingDeclarationIsUnknownButKnownSkuDifferenceStillRequiresReason() {
|
||||
var unknown = new WarehouseTransferItem(); unknown.setId(10L); unknown.setSkuCode("A");
|
||||
var known = new WarehouseTransferItem(); known.setId(11L); known.setSkuCode("B"); known.setDeclaredQuantity(7L);
|
||||
a.setSkuItemsJson("[{\"transferItemId\":10,\"skuCode\":\"A\",\"quantity\":4},{\"transferItemId\":11,\"skuCode\":\"B\",\"quantity\":7}]");
|
||||
assertFalse(service.hasSkuReceiptDifference(List.of(a), List.of(unknown, known)));
|
||||
known.setDeclaredQuantity(8L);
|
||||
assertTrue(service.hasSkuReceiptDifference(List.of(a), List.of(unknown, known)));
|
||||
known.setDeclaredQuantity(7L); unknown.setDeclaredQuantity(0L);
|
||||
assertTrue(service.hasSkuReceiptDifference(List.of(a), List.of(unknown, known)));
|
||||
}
|
||||
|
||||
/** 新箱必须先补客户预报,PDA不能绕过预报生成另一套箱号。 */
|
||||
@Test void extraReceiptRequiresForecastCartonFirst() {
|
||||
var extra = new WarehouseTransferRequests.CountCarton(null,11L,200L,new BigDecimal("10"),new BigDecimal("20"),new BigDecimal("30"),BigDecimal.ONE,List.of(new WarehouseTransferRequests.CountSku("B",4)));
|
||||
|
||||
+2
@@ -521,6 +521,8 @@ class WarehouseTransferServiceTest {
|
||||
transfer.setInboundCode("IN-1");
|
||||
transfer.setNeedRelabel(true);
|
||||
transfer.setPackageCount(1);
|
||||
var selected = new WarehouseTransferCarton();
|
||||
selected.setId(51L); selected.setCustomerCartonMark("CURRENT-BOX");
|
||||
when(inboundSnapshots.sourceCartons("TENANT-A", "WH-1", "IN-1"))
|
||||
.thenReturn(List.of(new InboundSnapshotMapper.SourceCarton("CURRENT-BOX", "SKU-1", null, null, null, null, null)));
|
||||
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.baiyue.lastleg.workflow;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
/** 通过真实Mapper验证出库取消和客户隔离,使用隔离内存库。 */
|
||||
class OrderWorkflowFactMapperTest {
|
||||
/** 有效出库、取消、实出库和差异卸柜均由持久化事实决定。 */
|
||||
@Test void cancellationAndCustomerAttribution() throws Exception {
|
||||
var ds=new JdbcDataSource();ds.setURL("jdbc:h2:mem:workflow-"+java.util.UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1");
|
||||
var db=new JdbcTemplate(ds);
|
||||
db.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR(30),source_inbound_id VARCHAR(30),source_region VARCHAR(20),order_type VARCHAR(30))");
|
||||
db.execute("CREATE TABLE ll_outbound_pallet_item(outbound_order_id BIGINT,customer_id VARCHAR(30),source_order_id BIGINT,destination_warehouse_code VARCHAR(30),destination_warehouse_id VARCHAR(30),dispatch_item_id BIGINT)");
|
||||
db.execute("CREATE TABLE ll_outbound_order(id BIGINT,customer_id VARCHAR(30),status VARCHAR(30),dispatch_order_id BIGINT)");
|
||||
db.execute("CREATE TABLE ll_delivery_order(outbound_order_id BIGINT,customer_id VARCHAR(30),status VARCHAR(30),dispatched_at TIMESTAMP,destination_warehouse_code VARCHAR(30),destination_warehouse_id VARCHAR(30),destination_type VARCHAR(20),delivery_address VARCHAR(100),postal_code VARCHAR(20))");
|
||||
db.execute("CREATE TABLE ll_dispatch_item(id BIGINT,customer_id VARCHAR(30),dispatch_order_id BIGINT,destination_type VARCHAR(20),delivery_address VARCHAR(100),postal_code VARCHAR(20))");
|
||||
db.execute("CREATE TABLE ll_inbound_plan(customer_id VARCHAR(30),source_order_id BIGINT,status VARCHAR(40))");
|
||||
db.update("INSERT INTO ll_order VALUES(1,'11','101','FIRSTLEG','FBA'),(2,'22','101','FIRSTLEG','FBA')");
|
||||
db.update("INSERT INTO ll_outbound_order VALUES(9,'11','WAIT_OUTBOUND',50),(10,'22','WAIT_OUTBOUND',51)");
|
||||
db.update("INSERT INTO ll_outbound_pallet_item VALUES(9,'11',1,'FBA1','3',60),(10,'22',2,'FBA1','3',61)");
|
||||
var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true);config.addMapper(OrderWorkflowFactMapper.class);
|
||||
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(ds);factory.setConfiguration(config);
|
||||
var mapper=new SqlSessionTemplate(factory.getObject()).getMapper(OrderWorkflowFactMapper.class);
|
||||
var sources=List.of(new OrderWorkflowFacts.Source(11,101));
|
||||
assertThat(mapper.read(sources)).singleElement().satisfies(f->{assertThat(f.tenantId()).isEqualTo(11);assertThat(f.outboundExists()).isTrue();assertThat(f.pendingOutbound()).isTrue();});
|
||||
db.update("UPDATE ll_outbound_order SET status='CANCELLED' WHERE id=9");
|
||||
assertThat(mapper.read(sources)).singleElement().satisfies(f->{assertThat(f.outboundExists()).isFalse();assertThat(f.pendingOutbound()).isFalse();});
|
||||
db.update("UPDATE ll_outbound_order SET status='OUTBOUNDED' WHERE id=9");
|
||||
db.update("INSERT INTO ll_delivery_order VALUES(9,'11','DELIVERING',CURRENT_TIMESTAMP,'FBA1','3','FBA',NULL,NULL)");
|
||||
db.update("INSERT INTO ll_inbound_plan VALUES('11',1,'PENDING_RECONCILIATION')");
|
||||
assertThat(mapper.read(sources)).singleElement().satisfies(f->{assertThat(f.outboundExists()).isTrue();assertThat(f.pendingOutbound()).isFalse();assertThat(f.delivering()).isTrue();assertThat(f.unloaded()).isTrue();});
|
||||
db.update("UPDATE ll_delivery_order SET destination_type='PRIVATE',destination_warehouse_code=NULL,destination_warehouse_id=NULL,delivery_address='Private A',postal_code='10001'");
|
||||
db.update("INSERT INTO ll_dispatch_item VALUES(60,'11',50,'PRIVATE','Private A','10001')");
|
||||
assertThat(mapper.read(sources)).singleElement().satisfies(f->assertThat(f.delivering()).isTrue());
|
||||
db.update("UPDATE ll_dispatch_item SET customer_id='22'");
|
||||
assertThat(mapper.read(sources)).singleElement().satisfies(f->assertThat(f.delivering()).isFalse());
|
||||
assertThat(mapper.read(List.of(new OrderWorkflowFacts.Source(33,101)))).isEmpty();
|
||||
// 相邻雪花ID超过浮点安全范围,必须按字符串精确匹配客户和来源。
|
||||
db.update("INSERT INTO ll_order VALUES(3,'2092814835845664769','2092814880129130499','FIRSTLEG','FBA'),(4,'2092814835845664768','2092814880129130499','FIRSTLEG','FBA'),(5,'2092814835845664769','2092814880129130498','FIRSTLEG','FBA')");
|
||||
assertThat(mapper.read(List.of(new OrderWorkflowFacts.Source(2092814835845664769L,2092814880129130499L))))
|
||||
.singleElement().satisfies(f->{assertThat(f.tenantId()).isEqualTo(2092814835845664769L);assertThat(f.inboundId()).isEqualTo(2092814880129130499L);});
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.lastleg.workflow;
|
||||
|
||||
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/** 内部事实入口不能被缺失或错误服务身份读取。 */
|
||||
class OrderWorkflowSecurityTest {
|
||||
/** 无效服务令牌在仓储查询前被拒绝。 */
|
||||
@Test void rejectsInvalidServiceIdentityBeforeReadingFacts() {
|
||||
var service=mock(OrderWorkflowFactService.class);
|
||||
var controller=new InternalOrderWorkflowFactController(new InternalServiceTokenGuard("test-service-token"),service);
|
||||
var request=new OrderWorkflowFacts.Request(List.of(new OrderWorkflowFacts.Source(11,101)));
|
||||
assertThatThrownBy(()->controller.read(null,request)).isInstanceOf(org.springframework.web.server.ResponseStatusException.class);
|
||||
assertThatThrownBy(()->controller.read("wrong-token",request)).isInstanceOf(org.springframework.web.server.ResponseStatusException.class);
|
||||
verifyNoInteractions(service);
|
||||
when(service.read(request)).thenReturn(List.of());
|
||||
assertThat(controller.read("test-service-token",request).successful()).isTrue();
|
||||
verify(service).read(request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user