修复废弃预留重放与预算到期时区
This commit is contained in:
+9
-2
@@ -163,7 +163,7 @@ class PdaCargoDiscardServiceTest {
|
||||
noWrites(); verifyNoInteractions(photoService);
|
||||
}
|
||||
/** 数量申请可仅使用一个托盘,也可跨托盘完成,剩余预留只释放一次。 */
|
||||
@ParameterizedTest @ValueSource(ints = {0, 2})
|
||||
@ParameterizedTest @ValueSource(ints = {0, 2, 3, 4})
|
||||
void quantityTargetUsesChosenPalletsAndReleasesUnusedReservations(int firstActual) {
|
||||
var stock = mock(com.baiyue.lastleg.discard.service.CargoDiscardStockService.class);
|
||||
ReflectionTestUtils.setField(service, "stockService", stock); scannedStock(stock);
|
||||
@@ -187,7 +187,14 @@ class PdaCargoDiscardServiceTest {
|
||||
if (firstActual > 0) {
|
||||
service.confirm(identity, "CD-1", "PALLET-1", request(firstActual));
|
||||
assertEquals(CargoDiscardService.APPROVED, application.getStatus());
|
||||
assertEquals(3, service.resolve(identity, "PALLET-2", "CD-1").remainingPackages());
|
||||
assertEquals(5-firstActual, service.resolve(identity, "PALLET-2", "CD-1").remainingPackages());
|
||||
// 整单尚未结束时重放首托,也只能核对原结果,不能再次占用剩余目标数量。
|
||||
var replay = service.confirm(identity, "CD-1", "PALLET-1", request(firstActual));
|
||||
assertEquals("COMPLETED", replay.status());
|
||||
assertEquals(5-firstActual, replay.remainingPackages());
|
||||
verify(stock, times(1)).discard("CUSTOMER", "OP", application, item, firstActual);
|
||||
verify(photoService).confirm(application, "PALLET-1", List.of(101L), true);
|
||||
assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", request(firstActual-1)));
|
||||
}
|
||||
var secondRequest = new PdaCargoDiscardDtos.Confirm(List.of(new PdaCargoDiscardDtos.ActualLine(12L, 5-firstActual, marks(12L, 5-firstActual))), List.of(101L));
|
||||
if (firstActual > 0) assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-2",
|
||||
|
||||
+33
-1
@@ -10,6 +10,8 @@ import com.baiyue.lastleg.inventory.mapper.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
@@ -26,13 +28,14 @@ class CargoDiscardStockServiceTest {
|
||||
final CargoDiscardCartonMapper links = mock(CargoDiscardCartonMapper.class);
|
||||
final CargoDiscardItemMapper items = mock(CargoDiscardItemMapper.class);
|
||||
final InventoryCommandService commands = mock(InventoryCommandService.class);
|
||||
final CargoDiscardRelabelStockService relabelStock = mock(CargoDiscardRelabelStockService.class);
|
||||
final CargoDiscardStockService service = new CargoDiscardStockService();
|
||||
final InventoryPosition position = new InventoryPosition();
|
||||
final InventoryBalance balance = new InventoryBalance();
|
||||
final CargoDiscardApplication application = new CargoDiscardApplication();
|
||||
CargoDiscardRequests.Item request(int count) { return new CargoDiscardRequests.Item(null, null, "SKU", count, 42L); }
|
||||
@BeforeEach void setup() {
|
||||
ReflectionTestUtils.setField(service, "relabelStock", mock(CargoDiscardRelabelStockService.class));
|
||||
ReflectionTestUtils.setField(service, "relabelStock", relabelStock);
|
||||
var repository = new com.baiyue.lastleg.discard.repository.CargoDiscardStockRepository();
|
||||
ReflectionTestUtils.setField(service, "repository", repository);
|
||||
ReflectionTestUtils.setField(repository, "stock", stock);
|
||||
@@ -86,6 +89,35 @@ class CargoDiscardStockServiceTest {
|
||||
var carton = new com.baiyue.lastleg.discard.model.CargoDiscardStockCartonRow();
|
||||
carton.setSourceType("INBOUND"); carton.setId(id); carton.setCartonMark(mark); carton.setSkuCode("SKU"); return carton;
|
||||
}
|
||||
/** 同一换标单的跨仓位预留全部保存后才暂停排车,单行入口同样保存暂停状态。 */
|
||||
@ParameterizedTest @ValueSource(ints = {1, 2})
|
||||
void relabelRowsStayEligibleUntilWholeReservationCompletes(int rowCount) {
|
||||
position.setAvailablePackages(0L); position.setHoldPackages(1L);
|
||||
balance.setAvailablePackages(0L); balance.setHoldPackages(2L);
|
||||
var second = new InventoryPosition(); second.setId(43L); second.setCustomerId("C1"); second.setBalanceId(51L);
|
||||
second.setOverseasWarehouseId("99"); second.setPalletNo("P-43"); second.setAvailablePackages(0L); second.setHoldPackages(1L);
|
||||
when(positions.findTenantPosition("C1", 43L)).thenReturn(second);
|
||||
when(stock.lockGenericPosition("C1", 43L)).thenReturn(second);
|
||||
var firstBox = carton(101L, "RELABEL-1"); firstBox.setSourceType("RELABEL");
|
||||
var secondBox = carton(102L, "RELABEL-2"); secondBox.setSourceType("RELABEL");
|
||||
when(relabelStock.cartons("C1", position)).thenReturn(List.of(firstBox));
|
||||
when(relabelStock.cartons("C1", second)).thenReturn(List.of(secondBox));
|
||||
var eligible = new java.util.concurrent.atomic.AtomicBoolean(true);
|
||||
when(relabelStock.claim(eq("C1"), anyLong())).thenAnswer(call -> eligible.get() ? 1 : 0);
|
||||
var savedLinks = new java.util.ArrayList<CargoDiscardCarton>();
|
||||
doAnswer(call -> { savedLinks.add(call.getArgument(0)); return 1; }).when(links).insert(any(CargoDiscardCarton.class));
|
||||
when(links.listApplication("C1", 11L)).thenAnswer(call -> List.copyOf(savedLinks));
|
||||
doAnswer(call -> { eligible.set(false); return null; }).when(relabelStock).synchronize(anyList(), eq("C1"));
|
||||
var firstRequest = new CargoDiscardRequests.Item(null, null, "SKU", 1, 42L, List.of(101L));
|
||||
if (rowCount == 1) service.reserve("C1", "OP", application, firstRequest, LocalDateTime.now());
|
||||
else service.reserveAll("C1", "OP", application, List.of(firstRequest,
|
||||
new CargoDiscardRequests.Item(null, null, "SKU", 1, 43L, List.of(102L))), LocalDateTime.now());
|
||||
assertFalse(eligible.get());
|
||||
assertEquals(rowCount, savedLinks.size());
|
||||
verify(commands, times(rowCount)).hold(eq("C1"), eq("OP"), any());
|
||||
verify(items, times(rowCount)).insert(any(CargoDiscardItem.class));
|
||||
verify(relabelStock).synchronize(argThat(values -> values.size() == rowCount), eq("C1"));
|
||||
}
|
||||
/** 同一混装实箱的两个 SKU 保留完整快照,物理仓位只冻结一次,实箱只锁定和保存一次,漏 SKU 时不得产生任何冻结。 */
|
||||
@Test void mixedTransferCartonRequiresAllSkusAndLocksPhysicalBoxOnce() {
|
||||
position.setAvailablePackages(1L); balance.setAvailablePackages(1L);
|
||||
|
||||
+8
-4
@@ -9,6 +9,7 @@ import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
@@ -18,7 +19,7 @@ class OrderBudgetPersistenceTest {
|
||||
OrderBudgetRepository repository;
|
||||
JdbcTemplate jdbc;
|
||||
@BeforeEach void setup() throws Exception {
|
||||
var ds = new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
var ds = new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1;INIT=SET TIME ZONE '+08:00'", "sa", "");
|
||||
jdbc = new JdbcTemplate(ds);
|
||||
String sql = Files.readString(Path.of("src/main/resources/db/lastleg.sql"));
|
||||
var match = Pattern.compile("(?s)CREATE TABLE IF NOT EXISTS ll_order_budget_reservation \\(.*?;").matcher(sql);
|
||||
@@ -39,17 +40,20 @@ class OrderBudgetPersistenceTest {
|
||||
assertThat(repository.lockOrder("10","procurement","CG1")).isNull();
|
||||
}
|
||||
@Test void selectsExpiredTerminalAndFailedReleaseButKeepsLiveOrders() {
|
||||
var expired = row(1,"FREEZE_PENDING");expired.setCreatedAt(LocalDateTime.now().minusMinutes(16));repository.insert(expired);
|
||||
var expired = row(1,"FREEZE_PENDING");expired.setCreatedAt(LocalDateTime.now(ZoneOffset.UTC).minusMinutes(16));repository.insert(expired);
|
||||
var terminal = row(2,"FROZEN");terminal.setOrderNo("CG2");repository.insert(terminal);
|
||||
jdbc.update("INSERT INTO ll_overseas_procurement_order VALUES('9','CG2','COMPLETED')");
|
||||
var failed = row(3,"RELEASE_FAILED");failed.setReleaseRequested(true);failed.setUpdatedAt(LocalDateTime.now().minusMinutes(2));repository.insert(failed);
|
||||
var failed = row(3,"RELEASE_FAILED");failed.setReleaseRequested(true);failed.setUpdatedAt(LocalDateTime.now(ZoneOffset.UTC).minusMinutes(2));repository.insert(failed);
|
||||
repository.insert(row(4,"FROZEN"));
|
||||
repository.insert(row(5,"FREEZE_PENDING"));
|
||||
var recentFailure = row(6,"RELEASE_FAILED");recentFailure.setReleaseRequested(true);repository.insert(recentFailure);
|
||||
assertThat(repository.releasable()).extracting(OrderBudget::getId).containsExactly(1L,2L,3L);
|
||||
}
|
||||
/** 测试数据与生产预占使用同一 UTC 时间,数据库会话保持东八区以覆盖时区差异。 */
|
||||
private OrderBudget row(long id,String status) {
|
||||
var row=new OrderBudget();row.setId(id);row.setCustomerId("9");row.setKind("procurement");row.setRequestKey("key"+id);
|
||||
row.setOriginalAmount(new BigDecimal("100"));row.setCurrency("USD");row.setAmountCny(new BigDecimal("800"));
|
||||
row.setStatus(status);row.setCommandVersion(1L);row.setReleaseRequested(false);
|
||||
row.setCreatedAt(LocalDateTime.now());row.setUpdatedAt(LocalDateTime.now());return row;
|
||||
row.setCreatedAt(LocalDateTime.now(ZoneOffset.UTC));row.setUpdatedAt(LocalDateTime.now(ZoneOffset.UTC));return row;
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -3,6 +3,8 @@ import com.baiyue.common.event.WalletCommandCompletedV1;
|
||||
import com.baiyue.lastleg.financefee.service.OverseasFeeCurrencyConverter;
|
||||
import com.baiyue.lastleg.wallet.WalletCommandOutboxService;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
@@ -63,6 +65,19 @@ class OrderBudgetServiceTest {
|
||||
assertThat(row.getStatus()).isEqualTo("RELEASE_PENDING");
|
||||
verify(repository).save(row);
|
||||
}
|
||||
/** 已绑定订单的预算释放后仍允许原键恢复原单,但客户、金额与币种不能借重放改写。 */
|
||||
@ParameterizedTest @ValueSource(strings={"RELEASE_PENDING","RELEASED","RELEASE_FAILED"})
|
||||
void releasedBoundBudgetReplaysOnlyTheOriginalOrder(String status) {
|
||||
row.setOrderNo("CG-1");row.setStatus(status);row.setReleaseRequested(true);
|
||||
var view=service.prepare("9","procurement","key",new BigDecimal("100"),"USD");
|
||||
assertThat(view.usable()).isFalse();assertThat(view.orderNo()).isEqualTo("CG-1");
|
||||
assertThat(service.submit("9","procurement","key",new BigDecimal("100"),"USD",()->"CG-1",v->v)).isEqualTo("CG-1");
|
||||
assertThatThrownBy(()->service.submit("9","procurement","key",new BigDecimal("100"),"USD",()->"CG-OTHER",v->v)).hasMessageContaining("其他订单");
|
||||
assertThatThrownBy(()->service.submit("9","procurement","key",new BigDecimal("200"),"USD",()->"CG-1",v->v)).hasMessageContaining("变化");
|
||||
assertThatThrownBy(()->service.submit("9","procurement","key",new BigDecimal("100"),"CNY",()->"CG-1",v->v)).hasMessageContaining("变化");
|
||||
assertThatThrownBy(()->service.submit("10","procurement","key",new BigDecimal("100"),"USD",()->"CG-1",v->v)).hasMessageContaining("校验");
|
||||
verifyNoInteractions(wallet);
|
||||
}
|
||||
private WalletCommandCompletedV1 result(String event,String operation,String outcome,String status,String error) {
|
||||
return new WalletCommandCompletedV1(event,operation,9L,OrderBudgetService.BUSINESS_TYPE,"11",outcome,status,new BigDecimal("800"),null,error,Instant.now(),"hold",null);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user