fix(lastleg): expose and validate individual return carton picks
This commit is contained in:
+165
@@ -0,0 +1,165 @@
|
||||
package com.baiyue.lastleg.delivery.service;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryRequests;
|
||||
import com.baiyue.lastleg.delivery.entity.*;
|
||||
import com.baiyue.lastleg.delivery.mapper.*;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
|
||||
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class ReturnOutboundPdaPersistenceTest {
|
||||
private JdbcTemplate jdbc;
|
||||
private DeliveryTransferService service;
|
||||
|
||||
@BeforeEach void setUp() throws Exception {
|
||||
var ds = new DriverManagerDataSource("jdbc:h2:mem:return-pda-" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
jdbc = new JdbcTemplate(ds);
|
||||
// Persist production entities through production MyBatis mappers, with real transactions.
|
||||
for (Class<?> entity : List.of(OutboundOrder.class, OutboundPalletItem.class, OutboundPdaTask.class,
|
||||
OutboundPdaPick.class, ReturnInventoryCarton.class)) {
|
||||
String columns = Arrays.stream(entity.getDeclaredFields()).filter(field -> !Modifier.isStatic(field.getModifiers()))
|
||||
.map(field -> field.getName().replaceAll("([A-Z])", "_$1").toLowerCase() + " "
|
||||
+ (field.getType() == String.class ? "VARCHAR(2000)" : field.getType() == LocalDateTime.class ? "TIMESTAMP"
|
||||
: field.getType() == java.math.BigDecimal.class ? "DECIMAL(20,6)" : "BIGINT")
|
||||
+ (field.getName().equals("id") ? " PRIMARY KEY" : "")).collect(Collectors.joining(","));
|
||||
jdbc.execute("CREATE TABLE " + entity.getAnnotation(TableName.class).value() + " (" + columns + ")");
|
||||
}
|
||||
var configuration = new MybatisConfiguration(); configuration.setMapUnderscoreToCamelCase(true);
|
||||
for (Class<?> mapper : List.of(OutboundOrderMapper.class, OutboundPalletItemMapper.class,
|
||||
OutboundPdaTaskMapper.class, OutboundPdaPickMapper.class, ReturnInventoryCartonMapper.class)) configuration.addMapper(mapper);
|
||||
var factory = new MybatisSqlSessionFactoryBean(); factory.setDataSource(ds); factory.setConfiguration(configuration);
|
||||
var session = new SqlSessionTemplate(factory.getObject());
|
||||
var target = new DeliveryTransferService();
|
||||
ReflectionTestUtils.setField(target, "outboundOrders", session.getMapper(OutboundOrderMapper.class));
|
||||
ReflectionTestUtils.setField(target, "outboundPallets", session.getMapper(OutboundPalletItemMapper.class));
|
||||
ReflectionTestUtils.setField(target, "pdaTasks", session.getMapper(OutboundPdaTaskMapper.class));
|
||||
ReflectionTestUtils.setField(target, "pdaPicks", session.getMapper(OutboundPdaPickMapper.class));
|
||||
ReflectionTestUtils.setField(target, "returnCartons", session.getMapper(ReturnInventoryCartonMapper.class));
|
||||
var numbers = mock(LastlegOrderNoGenerator.class);
|
||||
when(numbers.next(eq("RETURN_OUTBOUND"), eq("WH-1"), anyLong())).thenAnswer(call -> "RETU-" + call.getArgument(2));
|
||||
ReflectionTestUtils.setField(target, "orderNoGenerator", numbers);
|
||||
var proxy = new ProxyFactory(target); proxy.setProxyTargetClass(true);
|
||||
proxy.addAdvice(new TransactionInterceptor(new DataSourceTransactionManager(ds), new AnnotationTransactionAttributeSource()));
|
||||
service = (DeliveryTransferService) proxy.getProxy();
|
||||
carton(1, "C1", "WH-1", "RT-1", "BOX-A"); carton(2, "C1", "WH-1", "RT-1", "BOX-B");
|
||||
carton(3, "C2", "WH-1", "RT-2", "OTHER-CUSTOMER"); carton(4, "C1", "WH-2", "RT-1", "OTHER-WAREHOUSE");
|
||||
}
|
||||
private void carton(long id, String customer, String warehouse, String returnNo, String mark) {
|
||||
jdbc.update("INSERT INTO ll_return_inventory_carton(id,customer_id,overseas_warehouse_id,source_return_no,carton_mark,status,total_quantity) VALUES(?,?,?,?,?,'AVAILABLE',5)", id, customer, warehouse, returnNo, mark);
|
||||
}
|
||||
private com.baiyue.lastleg.delivery.dto.DeliveryResponses.PdaTask create(String key, String... marks) {
|
||||
return service.createReturnOutbound("C1", "operator", new DeliveryRequests.ReturnOutbound("recipient", "WH-1", "RT-1", List.of(marks), "fixture", key));
|
||||
}
|
||||
@Test void singleExistingShapeHasRealCartonScanRowWithoutInventedPalletOrPosition() {
|
||||
var task = create("single", "BOX-A");
|
||||
var detail = service.pdaTaskDetail("WH-1", task.taskNo());
|
||||
assertEquals(1, detail.items().size());
|
||||
assertEquals(1, detail.items().get(0).positions().size());
|
||||
var row = detail.items().get(0).positions().get(0);
|
||||
assertNull(row.inventoryPositionId()); assertNull(row.palletNo());
|
||||
assertEquals("BOX-A", row.cartonMark()); assertEquals("RT-1", row.inboundCode());
|
||||
assertEquals(1, row.availablePackages()); assertEquals(0, row.pickedPackages());
|
||||
assertEquals(0, jdbc.queryForObject("SELECT COUNT(*) FROM ll_outbound_pda_pick", Integer.class));
|
||||
}
|
||||
@Test void multiplePhysicalCartonsPersistSeparateIndependentlyScannableItems() {
|
||||
var task = create("multi", "BOX-A", "BOX-B");
|
||||
var detail = service.pdaTaskDetail("WH-1", task.taskNo());
|
||||
assertEquals(2, detail.items().size());
|
||||
assertTrue(detail.items().stream().allMatch(item -> item.packageCount() == 1 && item.positions().size() == 1));
|
||||
var a = detail.items().stream().filter(item -> item.positions().get(0).cartonMark().equals("BOX-A")).findFirst().orElseThrow();
|
||||
service.pdaPick("WH-1", "operator", task.taskNo(), new DeliveryRequests.PdaPick(a.outboundPalletItemId(), null, 1L, "pick-a", List.of("BOX-A")));
|
||||
var after = service.pdaTaskDetail("WH-1", task.taskNo());
|
||||
assertEquals(1, after.items().stream().mapToInt(item -> item.pickedPackageCount()).sum());
|
||||
assertEquals(1, after.items().stream().flatMap(item -> item.positions().stream()).mapToLong(row -> row.availablePackages()).sum());
|
||||
}
|
||||
@Test void wrongScanAndWrongReplayPayloadLeaveStoredInventoryUnchanged() {
|
||||
var task = create("scan", "BOX-A"); var item = service.pdaTaskDetail("WH-1", task.taskNo()).items().get(0);
|
||||
assertThrows(ResponseStatusException.class, () -> service.pdaPick("WH-1", "operator", task.taskNo(),
|
||||
new DeliveryRequests.PdaPick(item.outboundPalletItemId(), null, 1L, "bad", List.of("BOX-B"))));
|
||||
assertEquals("AVAILABLE", jdbc.queryForObject("SELECT status FROM ll_return_inventory_carton WHERE id=1", String.class));
|
||||
var valid = new DeliveryRequests.PdaPick(item.outboundPalletItemId(), null, 1L, "good", List.of("BOX-A"));
|
||||
service.pdaPick("WH-1", "operator", task.taskNo(), valid);
|
||||
service.pdaPick("WH-1", "operator", task.taskNo(), valid);
|
||||
assertThrows(ResponseStatusException.class, () -> service.pdaPick("WH-1", "operator", task.taskNo(),
|
||||
new DeliveryRequests.PdaPick(item.outboundPalletItemId(), null, 1L, "good", List.of("BOX-B"))));
|
||||
assertEquals(1, jdbc.queryForObject("SELECT COUNT(*) FROM ll_outbound_pda_pick", Integer.class));
|
||||
}
|
||||
@Test void customerWarehouseAndSourceReturnMismatchNeverAppearAsPickableRows() {
|
||||
var task = create("scope", "BOX-A");
|
||||
assertThrows(ResponseStatusException.class, () -> service.pdaTaskDetail("WH-2", task.taskNo()));
|
||||
for (String mark : List.of("OTHER-CUSTOMER", "OTHER-WAREHOUSE")) {
|
||||
assertThrows(ResponseStatusException.class, () -> create("bad-" + mark, mark));
|
||||
}
|
||||
for (String column : List.of("customer_id", "overseas_warehouse_id", "source_return_no")) {
|
||||
String original = jdbc.queryForObject("SELECT " + column + " FROM ll_return_inventory_carton WHERE id=1", String.class);
|
||||
jdbc.update("UPDATE ll_return_inventory_carton SET " + column + "='FOREIGN' WHERE id=1");
|
||||
assertThrows(ResponseStatusException.class, () -> service.pdaTaskDetail("WH-1", task.taskNo()));
|
||||
jdbc.update("UPDATE ll_return_inventory_carton SET " + column + "=? WHERE id=1", original);
|
||||
}
|
||||
}
|
||||
@Test void completedTaskDetailIsReadOnlyAndContainsNoPickableStock() {
|
||||
var task = create("complete", "BOX-A"); var item = service.pdaTaskDetail("WH-1", task.taskNo()).items().get(0);
|
||||
var pick = new DeliveryRequests.PdaPick(item.outboundPalletItemId(), null, 1L, "pick", List.of("BOX-A"));
|
||||
service.pdaPick("WH-1", "operator", task.taskNo(), pick);
|
||||
service.completePda("WH-1", task.taskNo(), new DeliveryRequests.PdaComplete("complete-key"));
|
||||
for (int i = 0; i < 2; i++) {
|
||||
var detail = service.pdaTaskDetail("WH-1", task.taskNo());
|
||||
assertEquals("COMPLETED", detail.task().status());
|
||||
assertTrue(detail.items().stream().flatMap(row -> row.positions().stream()).allMatch(row -> row.availablePackages() == 0));
|
||||
}
|
||||
assertThrows(ResponseStatusException.class, () -> service.pdaPick("WH-1", "operator", task.taskNo(),
|
||||
new DeliveryRequests.PdaPick(item.outboundPalletItemId(), null, 1L, "new-key", List.of("BOX-A"))));
|
||||
assertEquals(1, jdbc.queryForObject("SELECT COUNT(*) FROM ll_outbound_pda_pick", Integer.class));
|
||||
}
|
||||
|
||||
@Test void sourceChangedAfterDetailReadCannotBeShippedByTheOldRequest() {
|
||||
var task = create("source-change", "BOX-A");
|
||||
var item = service.pdaTaskDetail("WH-1", task.taskNo()).items().get(0);
|
||||
jdbc.update("UPDATE ll_return_inventory_carton SET source_return_no='RT-OTHER' WHERE id=1");
|
||||
assertThrows(ResponseStatusException.class, () -> service.pdaPick("WH-1", "operator", task.taskNo(),
|
||||
new DeliveryRequests.PdaPick(item.outboundPalletItemId(), null, 1L, "stale-source", List.of("BOX-A"))));
|
||||
assertEquals("AVAILABLE", jdbc.queryForObject("SELECT status FROM ll_return_inventory_carton WHERE id=1", String.class));
|
||||
assertEquals(0, jdbc.queryForObject("SELECT COUNT(*) FROM ll_outbound_pda_pick", Integer.class));
|
||||
assertEquals(0, jdbc.queryForObject("SELECT picked_package_count FROM ll_outbound_pallet_item", Integer.class));
|
||||
}
|
||||
|
||||
@Test void failedPickJournalRollsBackCartonAndTaskThenAllowsAnExactRetry() {
|
||||
var task = create("rollback", "BOX-A");
|
||||
var item = service.pdaTaskDetail("WH-1", task.taskNo()).items().get(0);
|
||||
var request = new DeliveryRequests.PdaPick(item.outboundPalletItemId(), null, 1L, "rollback-pick", List.of("BOX-A"));
|
||||
jdbc.execute("ALTER TABLE ll_outbound_pda_pick ADD CONSTRAINT fail_pick CHECK(idempotency_key <> 'rollback-pick')");
|
||||
assertThrows(RuntimeException.class, () -> service.pdaPick("WH-1", "operator", task.taskNo(), request));
|
||||
assertEquals("AVAILABLE", jdbc.queryForObject("SELECT status FROM ll_return_inventory_carton WHERE id=1", String.class));
|
||||
assertEquals(0, jdbc.queryForObject("SELECT COUNT(*) FROM ll_outbound_pda_pick", Integer.class));
|
||||
var unchanged = service.pdaTaskDetail("WH-1", task.taskNo());
|
||||
assertEquals("PENDING", unchanged.task().status());
|
||||
assertEquals(1, unchanged.items().get(0).positions().get(0).availablePackages());
|
||||
jdbc.execute("ALTER TABLE ll_outbound_pda_pick DROP CONSTRAINT fail_pick");
|
||||
service.pdaPick("WH-1", "operator", task.taskNo(), request);
|
||||
assertEquals("OUTBOUNDED", jdbc.queryForObject("SELECT status FROM ll_return_inventory_carton WHERE id=1", String.class));
|
||||
assertEquals(1, jdbc.queryForObject("SELECT COUNT(*) FROM ll_outbound_pda_pick", Integer.class));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user