优化
This commit is contained in:
+13
-1
@@ -8,6 +8,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -18,6 +20,7 @@ class PlatformOceanBookingControllerContractTest {
|
||||
.containsExactly("/api/platform/v1/ocean-bookings");
|
||||
Map<String, String> permissions = Map.ofEntries(
|
||||
Map.entry("page", "platform:firstleg:ocean-booking:list"),
|
||||
Map.entry("channels", "platform:firstleg:ocean-booking:list"),
|
||||
Map.entry("detail", "platform:firstleg:ocean-booking:list"),
|
||||
Map.entry("create", "platform:firstleg:ocean-booking:create"),
|
||||
Map.entry("updateDraft", "platform:firstleg:ocean-booking:edit"),
|
||||
@@ -34,7 +37,7 @@ class PlatformOceanBookingControllerContractTest {
|
||||
Method method = method(name);
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('" + permission + "')");
|
||||
if (!name.equals("page") && !name.equals("detail")) {
|
||||
if (!name.equals("page") && !name.equals("detail") && !name.equals("channels")) {
|
||||
assertThat(method.getAnnotation(BusinessAudit.class)).isNotNull();
|
||||
}
|
||||
});
|
||||
@@ -49,6 +52,15 @@ class PlatformOceanBookingControllerContractTest {
|
||||
.allMatch(parameter -> parameter.getName().equals("oceanBookingId"))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void openApiFreezesServerResolvedChannelContract() throws Exception {
|
||||
String yaml = Files.readString(Path.of("src/main/resources/openapi/ocean-bookings-v1.yaml"));
|
||||
assertThat(yaml).contains("/api/platform/v1/ocean-bookings/channels:",
|
||||
"originWarehouseId", "platform:firstleg:ocean-booking:list", "ChannelOption")
|
||||
.contains("required: [customerId, customerName, channelCode, warehouseId, warehouseName")
|
||||
.doesNotContain("required: [customerId, customerName, channelCode, channelName");
|
||||
}
|
||||
|
||||
private static Method method(String name) {
|
||||
return Arrays.stream(PlatformOceanBookingController.class.getDeclaredMethods())
|
||||
.filter(method -> method.getName().equals(name)).findFirst().orElseThrow();
|
||||
|
||||
+131
@@ -3,9 +3,14 @@ package com.baiyue.firstleg.oceanbooking.service;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.ActionRequest;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.ChangeRequest;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.ContainerRequirementRequest;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.CreateRequest;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.Detail;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.DraftData;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.AllocationRequest;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.PartyRequest;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.ReleaseRequest;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.UpdateDraftRequest;
|
||||
import com.baiyue.firstleg.service.LoadingPickService;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
|
||||
@@ -14,6 +19,8 @@ import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingOperationEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||
import com.baiyue.firstleg.repository.PriceBookQueryRepository;
|
||||
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -23,7 +30,9 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@@ -32,6 +41,7 @@ import static org.mockito.Mockito.*;
|
||||
|
||||
class OceanBookingServiceTest {
|
||||
@Mock private OceanBookingRepository repository;
|
||||
@Mock private PriceBookQueryRepository priceBooks;
|
||||
@Mock private LoadingPickService loadingPickService;
|
||||
@InjectMocks private OceanBookingService service;
|
||||
|
||||
@@ -172,12 +182,133 @@ class OceanBookingServiceTest {
|
||||
verify(repository, never()).insert(any(OceanBookingContainerAllocationEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listsOnlyChannelsFromCurrentActivePriceBook() {
|
||||
ChannelRow channel = channel("SEA-US", "美西海运");
|
||||
when(priceBooks.activeVersionId()).thenReturn(51L);
|
||||
when(priceBooks.channels(51L, 701L)).thenReturn(List.of(channel));
|
||||
when(priceBooks.channelTaxModes(51L, 701L)).thenReturn(List.of());
|
||||
|
||||
var result = service.channels(701L);
|
||||
|
||||
assertThat(result).singleElement().satisfies(value -> {
|
||||
assertThat(value.channelCode()).isEqualTo("SEA-US");
|
||||
assertThat(value.channelName()).isEqualTo("美西海运");
|
||||
assertThat(value.priceBookVersionId()).isEqualTo(51L);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void freezesAuthoritativeChannelNameWhenCreatingDraft() {
|
||||
ChannelRow channel = channel("SEA-US", "价格簿美西海运");
|
||||
when(priceBooks.activeVersionId()).thenReturn(51L);
|
||||
when(priceBooks.channels(51L, 701L)).thenReturn(List.of(channel));
|
||||
AtomicReference<OceanBookingEntity> inserted = new AtomicReference<>();
|
||||
doAnswer(invocation -> { inserted.set(invocation.getArgument(0)); return null; })
|
||||
.when(repository).insert(any(OceanBookingEntity.class));
|
||||
when(repository.find(anyLong())).thenAnswer(invocation -> inserted.get());
|
||||
|
||||
Detail result = service.create(9L, "admin", "create-channel", new CreateRequest(0, draft()));
|
||||
|
||||
assertThat(result.oceanBooking().getChannelCode()).isEqualTo("SEA-US");
|
||||
assertThat(result.oceanBooking().getChannelName()).isEqualTo("价格簿美西海运");
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaysCreateAfterChannelIsNoLongerActive() throws Exception {
|
||||
ChannelRow channel = channel("SEA-US", "价格簿美西海运");
|
||||
CreateRequest request = new CreateRequest(0, draft());
|
||||
when(priceBooks.activeVersionId()).thenReturn(51L, (Long) null);
|
||||
when(priceBooks.channels(51L, 701L)).thenReturn(List.of(channel));
|
||||
AtomicReference<OceanBookingEntity> inserted = new AtomicReference<>();
|
||||
doAnswer(invocation -> { inserted.set(invocation.getArgument(0)); return null; })
|
||||
.when(repository).insert(any(OceanBookingEntity.class));
|
||||
when(repository.find(anyLong())).thenAnswer(invocation -> inserted.get());
|
||||
|
||||
Detail created = service.create(9L, "admin", "create-replay-channel", request);
|
||||
|
||||
OceanBookingOperationEntity operation = new OceanBookingOperationEntity();
|
||||
operation.setRequestHash(hashForCreate(request)); operation.setStatus("SUCCESS");
|
||||
operation.setResultJson(new ObjectMapper().findAndRegisterModules().writeValueAsString(created));
|
||||
when(repository.claimOperation(isNull(), eq(9L), eq("CREATE"), eq("create-replay-channel"), anyString()))
|
||||
.thenReturn(false);
|
||||
when(repository.operation(9L, "CREATE", "create-replay-channel")).thenReturn(operation);
|
||||
|
||||
Detail replay = service.create(9L, "admin", "create-replay-channel", request);
|
||||
|
||||
assertThat(replay.oceanBooking().getChannelName()).isEqualTo("价格簿美西海运");
|
||||
verify(priceBooks, times(1)).activeVersionId();
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaysDraftUpdateAfterChannelIsNoLongerActive() throws Exception {
|
||||
UpdateDraftRequest request = new UpdateDraftRequest(5, draft());
|
||||
Detail expected = new Detail(booking("DRAFT", 6), List.of(), List.of(), List.of(), List.of());
|
||||
OceanBookingOperationEntity operation = new OceanBookingOperationEntity();
|
||||
operation.setRequestHash(hashForUpdate(request)); operation.setStatus("SUCCESS");
|
||||
operation.setResultJson(new ObjectMapper().findAndRegisterModules().writeValueAsString(expected));
|
||||
when(repository.claimOperation(eq(1L), eq(9L), eq("UPDATE_DRAFT"), eq("update-replay-channel"), anyString()))
|
||||
.thenReturn(false);
|
||||
when(repository.operation(9L, "UPDATE_DRAFT", "update-replay-channel")).thenReturn(operation);
|
||||
|
||||
Detail replay = service.updateDraft(1L, 9L, "admin", "update-replay-channel", request);
|
||||
|
||||
assertThat(replay.oceanBooking().getVersion()).isEqualTo(6);
|
||||
verifyNoInteractions(priceBooks);
|
||||
verify(repository, never()).find(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsConfirmationWhenFrozenChannelIsNoLongerActive() {
|
||||
OceanBookingEntity submitted = booking("SUBMITTED", 3);
|
||||
submitted.setWarehouseId(701L); submitted.setWarehouseName("深圳仓"); submitted.setChannelName("美西海运");
|
||||
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
|
||||
requirement.setCapacityCbm(java.math.BigDecimal.ONE); requirement.setMaxWeightKg(java.math.BigDecimal.ONE);
|
||||
when(repository.find(1L)).thenReturn(submitted);
|
||||
when(repository.requirements(1L)).thenReturn(List.of(requirement));
|
||||
when(priceBooks.activeVersionId()).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> service.confirm(1L, 9L, "admin", "confirm-channel", new ActionRequest(3, "确认")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("ACTIVE");
|
||||
|
||||
verify(repository, never()).transition(anyLong(), anyString(), anyString(), any(), any(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
private static ChannelRow channel(String code, String name) {
|
||||
ChannelRow value = new ChannelRow(); value.setPriceBookVersionId(51L); value.setChannelCode(code);
|
||||
value.setChannelName(name); value.setPriceModel("FBA_WAREHOUSE_RATE"); return value;
|
||||
}
|
||||
|
||||
private static DraftData draft() {
|
||||
List<PartyRequest> parties = List.of(
|
||||
new PartyRequest("SHIPPER", "发货人", null, null, null),
|
||||
new PartyRequest("CONSIGNEE", "收货人", null, null, null),
|
||||
new PartyRequest("NOTIFY_PARTY", "通知人", null, null, null));
|
||||
List<ContainerRequirementRequest> requirements = List.of(
|
||||
new ContainerRequirementRequest("40HQ", "DRY", BigDecimal.ONE, BigDecimal.TEN, 1));
|
||||
return new DraftData(100L, "客户", "SEA-US", 701L, "深圳仓", null,
|
||||
81L, "船公司", null, null, null, null, "CNSZX", "深圳", "USLAX", "洛杉矶",
|
||||
null, null, null, null, null, null, parties, requirements);
|
||||
}
|
||||
|
||||
private String hashForSubmit() throws Exception {
|
||||
var method = OceanBookingService.class.getDeclaredMethod("hash", String.class, Long.class, Object.class);
|
||||
method.setAccessible(true);
|
||||
return (String) method.invoke(service, "SUBMIT", 1L, new ActionRequest(2, "提交"));
|
||||
}
|
||||
|
||||
private String hashForCreate(CreateRequest request) throws Exception {
|
||||
var method = OceanBookingService.class.getDeclaredMethod("hash", String.class, Long.class, Object.class);
|
||||
method.setAccessible(true);
|
||||
return (String) method.invoke(service, "CREATE", (Long) null, request);
|
||||
}
|
||||
|
||||
private String hashForUpdate(UpdateDraftRequest request) throws Exception {
|
||||
var method = OceanBookingService.class.getDeclaredMethod("hash", String.class, Long.class, Object.class);
|
||||
method.setAccessible(true);
|
||||
return (String) method.invoke(service, "UPDATE_DRAFT", 1L, request);
|
||||
}
|
||||
|
||||
private static OceanBookingEntity booking(String status, int version) {
|
||||
OceanBookingEntity value = new OceanBookingEntity(); value.setId(1L); value.setBookingNo("OB1");
|
||||
value.setStatus(status); value.setCustomerId(100L); value.setCustomerName("客户");
|
||||
|
||||
Reference in New Issue
Block a user