fix(discard): recover completed PDA confirmation without duplicate writes

This commit is contained in:
2026-10-05 08:10:43 +08:00
parent 5864e62443
commit 113d2b1fec
2 changed files with 148 additions and 4 deletions
@@ -0,0 +1,122 @@
package com.baiyue.lastleg.discard.pda.service;
import com.baiyue.lastleg.discard.entity.CargoDiscardApplication;
import com.baiyue.lastleg.discard.entity.CargoDiscardItem;
import com.baiyue.lastleg.discard.mapper.CargoDiscardApplicationMapper;
import com.baiyue.lastleg.discard.mapper.CargoDiscardItemMapper;
import com.baiyue.lastleg.discard.mapper.CargoDiscardCartonMapper;
import com.baiyue.lastleg.discard.pda.dto.PdaCargoDiscardDtos;
import com.baiyue.lastleg.discard.service.CargoDiscardService;
import com.baiyue.lastleg.discard.service.CargoDiscardInventoryAdjustmentService;
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
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.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
class PdaCargoDiscardServiceTest {
private final CargoDiscardApplicationMapper applications = mock(CargoDiscardApplicationMapper.class);
private final CargoDiscardItemMapper items = mock(CargoDiscardItemMapper.class);
private final CargoDiscardCartonMapper cartons = mock(CargoDiscardCartonMapper.class);
private final DropshipOrderProfileMapper profiles = mock(DropshipOrderProfileMapper.class);
private final CargoDiscardInventoryAdjustmentService adjustments = mock(CargoDiscardInventoryAdjustmentService.class);
private final CargoDiscardService discardService = mock(CargoDiscardService.class);
private final ReturnInventoryCartonMapper returnCartons = mock(ReturnInventoryCartonMapper.class);
private PdaCargoDiscardService service;
private CargoDiscardApplication application;
private CargoDiscardItem item;
private final PdaWarehouseIdentity identity = new PdaWarehouseIdentity("OP", "operator", 99L);
private PdaCargoDiscardDtos.Confirm request(int actual) { return new PdaCargoDiscardDtos.Confirm(List.of(new PdaCargoDiscardDtos.ActualLine(11L, actual))); }
@BeforeEach void setup() {
service = new PdaCargoDiscardService();
ReflectionTestUtils.setField(service, "applications", applications);
ReflectionTestUtils.setField(service, "items", items);
ReflectionTestUtils.setField(service, "cartons", cartons);
ReflectionTestUtils.setField(service, "profiles", profiles);
ReflectionTestUtils.setField(service, "adjustments", adjustments);
ReflectionTestUtils.setField(service, "discardService", discardService);
ReflectionTestUtils.setField(service, "returnCartons", returnCartons);
application = new CargoDiscardApplication(); application.setId(1L); application.setDiscardNo("CD-1");
application.setCustomerId("CUSTOMER"); application.setOverseasWarehouseId("99");
application.setStatus(CargoDiscardService.APPROVED); application.setFeePerPackage(new BigDecimal("5.00"));
item = new CargoDiscardItem(); item.setId(11L); item.setApplicationId(1L); item.setCustomerId("CUSTOMER");
item.setProfileId(22L); item.setSkuCode("SKU"); item.setDiscardPackages(1); item.setPdaStatus("PENDING");
DropshipOrderProfile profile = new DropshipOrderProfile(); profile.setId(22L); profile.setCustomerId("CUSTOMER"); profile.setPalletNo("PALLET-1");
when(applications.findNoByWarehouse("CD-1", "99")).thenReturn(application);
when(items.listApplication("CUSTOMER", 1L)).thenAnswer(invocation -> List.of(item));
when(items.selectById(11L)).thenReturn(item);
when(profiles.selectById(22L)).thenReturn(profile);
when(cartons.listApplication("CUSTOMER", 1L)).thenReturn(List.of());
doAnswer(invocation -> { application.setStatus(invocation.getArgument(3)); return null; })
.when(discardService).updateLinkedStatus(anyString(), same(application), anyString(), anyString(), anyString(), anyString());
}
private void finished(String status) {
application.setStatus(status); item.setPdaStatus("COMPLETED"); item.setActualDiscardPackages(1);
item.setPdaOperatorId("original"); item.setPdaCompletedAt(LocalDateTime.of(2026, 10, 5, 1, 2));
}
private void noWrites() {
verifyNoInteractions(adjustments, discardService);
verify(items, never()).updateById(any(CargoDiscardItem.class));
verify(applications, never()).updateById(any(CargoDiscardApplication.class));
}
@Test void lastPalletActualConfirmationReplaysAfterPdaCompletionWithoutDeductionOrHistoryAgain() {
var first = service.confirm(identity, "CD-1", "PALLET-1", request(1));
assertEquals("COMPLETED", first.status());
assertEquals(1, first.skus().get(0).actualPackages());
assertEquals("COMPLETED", first.skus().get(0).status());
assertEquals(CargoDiscardService.PDA_COMPLETED, application.getStatus());
var completedAt = item.getPdaCompletedAt(); var result = service.confirm(identity, "CD-1", "PALLET-1", request(1));
assertEquals("COMPLETED", result.status()); assertEquals(1, result.skus().get(0).actualPackages());
assertEquals(completedAt, item.getPdaCompletedAt());
verify(adjustments, times(1)).discardPalletItem(eq("CUSTOMER"), same(application), same(item), anyList(), eq(1), eq("OP"), any());
verify(items, times(1)).updateById(item);
verify(discardService, times(1)).updateLinkedStatus(anyString(), same(application), anyString(), anyString(), anyString(), anyString());
}
@ParameterizedTest @ValueSource(strings = {"PDA_COMPLETED", "COMPLETED", "APPROVED"})
void exactFinishedPalletIsReadOnlyInAllowedStates(String status) {
finished(status); var result = service.confirm(identity, "CD-1", "PALLET-1", request(1));
assertEquals("COMPLETED", result.status()); assertEquals(status, application.getStatus()); noWrites();
}
@ParameterizedTest @ValueSource(strings = {"PDA_COMPLETED", "COMPLETED"})
void finishedQuantityCannotChange(String status) { finished(status); assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", request(0))); noWrites(); }
@Test void missingOrForeignLineAndWrongPalletCannotRecover() {
finished("PDA_COMPLETED");
assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", new PdaCargoDiscardDtos.Confirm(List.of())));
assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", new PdaCargoDiscardDtos.Confirm(List.of(new PdaCargoDiscardDtos.ActualLine(999L, 1)))));
assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "OTHER-PALLET", request(1))); noWrites();
}
@Test void foreignWarehouseHasNoApplication() {
finished("PDA_COMPLETED"); assertThrows(ResponseStatusException.class, () -> service.confirm(new PdaWarehouseIdentity("OP", "operator", 100L), "CD-1", "PALLET-1", request(1))); noWrites();
}
@Test void foreignCustomerPhysicalPalletCannotRecover() {
finished("PDA_COMPLETED"); DropshipOrderProfile other = new DropshipOrderProfile(); other.setCustomerId("OTHER"); other.setPalletNo("PALLET-1");
when(profiles.selectById(22L)).thenReturn(other);
assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", request(1))); noWrites();
}
@ParameterizedTest @ValueSource(strings = {"CANCELLED", "REJECTED", "PENDING_REVIEW"})
void cancelledRejectedOrUnapprovedCannotPretendSuccess(String status) { finished(status); assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", request(1))); noWrites(); }
@ParameterizedTest @ValueSource(strings = {"PDA_COMPLETED", "COMPLETED"})
void unfinishedItemInTerminalApplicationCannotRecover(String status) { application.setStatus(status); assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", request(1))); noWrites(); }
@Test void terminalApplicationWithAnotherUnfinishedPalletCannotRecover() {
finished("PDA_COMPLETED"); CargoDiscardItem pending = new CargoDiscardItem(); pending.setId(12L); pending.setApplicationId(1L);
pending.setCustomerId("CUSTOMER"); pending.setProfileId(23L); pending.setSkuCode("OTHER-SKU"); pending.setDiscardPackages(1); pending.setPdaStatus("PENDING");
DropshipOrderProfile other = new DropshipOrderProfile(); other.setId(23L); other.setCustomerId("CUSTOMER"); other.setPalletNo("PALLET-2");
when(items.listApplication("CUSTOMER", 1L)).thenReturn(List.of(item, pending)); when(profiles.selectById(23L)).thenReturn(other);
assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", request(1))); noWrites();
}
@Test void duplicateLineCannotRecover() { finished("COMPLETED"); assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", new PdaCargoDiscardDtos.Confirm(List.of(new PdaCargoDiscardDtos.ActualLine(11L, 1), new PdaCargoDiscardDtos.ActualLine(11L, 1))))); noWrites(); }
@Test void loadedForeignCustomerItemCannotRecover() { finished("COMPLETED"); item.setCustomerId("OTHER"); assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", request(1))); noWrites(); }
@Test void loadedDifferentApplicationItemCannotRecover() { finished("COMPLETED"); item.setApplicationId(2L); assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", request(1))); noWrites(); }
}