feat(firstleg): confirm domestic departure by container

This commit is contained in:
2026-08-26 12:05:42 +08:00
parent 9ea2aa46ec
commit 8eeb52ba77
11 changed files with 583 additions and 292 deletions
@@ -10,9 +10,25 @@ class OutboundDispatchReadMapperContractTest {
@Test
void bindingProjectsStableCabinetAndRealContainerNumbersSeparately() throws Exception {
String sql = String.join("\n", OutboundDispatchReadMapper.class
.getMethod("bindings", Long.class, Long.class, Long.class)
.getMethod("bindings", long.class)
.getAnnotation(Select.class).value());
assertThat(sql).contains("booking.cabinet_no cabinetNo", "booking.container_no containerNo");
assertThat(sql)
.contains("booking.cabinet_no cabinetNo", "booking.container_no containerNo")
.contains("booking.id=#{containerBookingId}")
.doesNotContain("#{orderId}", "#{inboundId}");
}
@Test
void actionQueryLocksEveryRealInboundBindingForTheContainer() throws Exception {
String sql = String.join("\n", OutboundDispatchReadMapper.class
.getMethod("bindingsForUpdate", long.class)
.getAnnotation(Select.class).value());
assertThat(sql)
.contains("booking.id=#{containerBookingId}")
.contains("item.deleted=0", "inbound.deleted=0")
.contains("FOR UPDATE")
.doesNotContain("LIMIT 1");
}
@Test
@@ -36,14 +52,19 @@ class OutboundDispatchReadMapperContractTest {
}
@Test
void oceanAndTruckQueriesUseTenantIsolationKey() throws Exception {
String oceanSql = OutboundDispatchReadMapper.class.getMethod("ocean", long.class, long.class)
void oceanAndTruckQueriesAreResolvedOnceFromTheContainerRoot() throws Exception {
String oceanSql = OutboundDispatchReadMapper.class.getMethod("ocean", long.class)
.getAnnotation(Select.class).value()[0];
String truckSql = OutboundDispatchReadMapper.class.getMethod("truck", long.class, long.class, long.class)
String truckSql = OutboundDispatchReadMapper.class.getMethod("truck", long.class, long.class)
.getAnnotation(Select.class).value()[0];
assertThat(oceanSql).contains("booking.customer_id=#{tenantId}").doesNotContain("#{customerId}");
assertThat(truckSql).contains("customer_id=#{tenantId}").doesNotContain("#{customerId}");
assertThat(oceanSql)
.contains("allocation.container_booking_id=#{containerBookingId}")
.contains("booking.customer_id customerId")
.doesNotContain("#{tenantId}");
assertThat(truckSql)
.contains("container_booking_id=#{containerBookingId}", "loading_plan_id=#{loadingPlanId}")
.doesNotContain("#{tenantId}");
}
@Test
@@ -9,9 +9,11 @@ import com.baiyue.firstleg.outbound.dispatch.mapper.OutboundDispatchReadMapper;
import com.baiyue.firstleg.outbound.dispatch.repository.OutboundDispatchRepository;
import com.baiyue.firstleg.outbound.dispatch.service.OutboundDispatchService;
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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;
@@ -19,159 +21,191 @@ import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
class OutboundDispatchServiceTest {
private final OutboundDispatchRepository repository = mock(OutboundDispatchRepository.class);
private OutboundDispatchRepository repository;
@BeforeEach
void setUp() {
repository = mock(OutboundDispatchRepository.class);
when(repository.timeline(any(), any(), any())).thenReturn(List.of());
}
@Test
void gateFailsClosedWhenCustomsAndScheduleModulesAreUnavailable() {
OutboundDispatchService service = new OutboundDispatchService(repository, List.of());
stubReadyChain();
void gateAggregatesEveryRealInboundAndFailsClosedWhenOneBindingFails() {
OutboundDispatchService service = serviceWithoutCarrier();
stubContainer();
when(repository.evidence(7L, 12L)).thenReturn(new OutboundDispatchReadMapper.Evidence(4, 1));
GateResult result = service.checkGate(new Query(null, 11L, 22L));
ContainerGateResult result = service.checkGate(22L);
assertThat(result.passed()).isFalse();
assertThat(result.items()).filteredOn(item -> !item.passed())
.extracting(GateItem::code).containsExactlyInAnyOrder("CUSTOMS_CLEARANCE", "SAILING_SCHEDULE");
assertThat(result.inbounds()).hasSize(2);
assertThat(result.inbounds().get(1).gate().items())
.filteredOn(item -> "LOADING_EVIDENCE_COMPLETE".equals(item.code()))
.extracting(GateItem::passed).containsExactly(false);
}
@Test
void aggregateUsesTenantKeyWhenCustomerEntityIdDiffers() {
OutboundDispatchService service = new OutboundDispatchService(repository, List.of());
stubReadyChain();
void detailReturnsOneVersionAndAllInboundBindings() {
OutboundDispatchService service = serviceWithoutCarrier();
stubContainer();
OutboundDispatchEntity first = state(31L, 11L, "WAITING_FOR_LOADING", 3);
OutboundDispatchEntity second = state(32L, 12L, "WAITING_FOR_LOADING", 3);
when(repository.dispatch(7L, 11L, 22L)).thenReturn(first);
when(repository.dispatch(7L, 12L, 22L)).thenReturn(second);
service.checkGate(new Query(null, 11L, 22L));
Detail detail = service.detail(22L);
verify(repository).ocean(22L, 7L);
verify(repository).truck(22L, 44L, 7L);
verify(repository, never()).ocean(22L, 8L);
verify(repository, never()).truck(22L, 44L, 8L);
assertThat(detail.aggregate().inbounds()).extracting(InboundBinding::inboundId).containsExactly(11L, 12L);
assertThat(detail.aggregate().dispatchStatus()).isEqualTo("WAITING_FOR_LOADING");
assertThat(detail.aggregate().version()).isEqualTo(3);
}
@Test
void confirmAdvancesDispatchWithoutMutatingLoadedInbound() {
OutboundDispatchService service = new OutboundDispatchService(repository,
List.of(passing("CUSTOMS_CLEARANCE"), passing("SAILING_SCHEDULE")));
stubReadyChain();
OutboundDispatchEntity state = new OutboundDispatchEntity();
state.setId(33L); state.setTenantId(7L); state.setOrderId(10L); state.setInboundId(11L);
state.setContainerBookingId(22L); state.setStatus("READY_FOR_DISPATCH"); state.setVersion(0);
when(repository.dispatch(7L, 11L, 22L)).thenReturn(state);
when(repository.claim(eq(9L), eq("CONFIRM_DISPATCH"), eq("confirm-1"), anyString())).thenReturn(true);
when(repository.confirm(33L, 0, 9L, "admin", "ok")).thenReturn(1);
when(repository.completeOperation(33L, 9L, "CONFIRM_DISPATCH", "confirm-1")).thenReturn(1);
when(repository.timeline(any(), any(), any())).thenReturn(List.of());
service.confirm(9L, "admin", "confirm-1", new ActionRequest(11L, 22L, 0, "ok"));
verify(repository).confirm(33L, 0, 9L, "admin", "ok");
verify(repository).history(argThat(history -> "CONFIRM_DISPATCH".equals(history.getAction())
&& "WAITING_FOR_LOADING".equals(history.getToStatus())));
verify(repository).completeOperation(33L, 9L, "CONFIRM_DISPATCH", "confirm-1");
}
@Test
void confirmCreatesCanonicalCarrierScheduleTaskInTheSameTransaction() {
void confirmLocksAndAdvancesAllBindingsButWritesOneHistoryAndOneShippingFact() {
CarrierScheduleQueryService carrierSchedules = mock(CarrierScheduleQueryService.class);
OutboundDispatchService service = new OutboundDispatchService(repository,
List.of(passing("CUSTOMS_CLEARANCE"), passing("SAILING_SCHEDULE")), carrierSchedules);
stubReadyChain();
OutboundDispatchEntity state = new OutboundDispatchEntity();
state.setId(33L); state.setTenantId(7L); state.setInboundId(11L);
state.setContainerBookingId(22L); state.setStatus("READY_FOR_DISPATCH"); state.setVersion(0);
when(repository.dispatch(7L, 11L, 22L)).thenReturn(state);
when(repository.claim(eq(9L), eq("CONFIRM_DISPATCH"), eq("confirm-carrier"), anyString())).thenReturn(true);
when(repository.confirm(33L, 0, 9L, "admin", null)).thenReturn(1);
when(repository.completeOperation(33L, 9L, "CONFIRM_DISPATCH", "confirm-carrier")).thenReturn(1);
when(repository.timeline(any(), any(), any())).thenReturn(List.of());
OutboundDispatchService service = service(carrierSchedules);
stubContainer();
when(repository.lockBindings(22L)).thenReturn(bindings());
OutboundDispatchEntity first = state(31L, 11L, "READY_FOR_DISPATCH", 0);
OutboundDispatchEntity second = state(32L, 12L, "READY_FOR_DISPATCH", 0);
when(repository.dispatchLocked(7L, 11L, 22L)).thenReturn(first);
when(repository.dispatchLocked(7L, 12L, 22L)).thenReturn(second);
when(repository.claim(eq(9L), eq("CONFIRM_DISPATCH"), eq("confirm-container"), anyString())).thenReturn(true);
when(repository.confirm(anyLong(), eq(0), eq(9L), eq("admin"), eq("ok"))).thenReturn(1);
when(repository.completeOperation(31L, 9L, "CONFIRM_DISPATCH", "confirm-container")).thenReturn(1);
service.confirm(9L, "admin", "confirm-carrier", new ActionRequest(11L, 22L, 0, null));
service.confirm(22L, 9L, "admin", "confirm-container", new ActionRequest(0, "ok"));
verify(carrierSchedules).ensureFromOutboundConfirm(7L, 66L, 22L, 9L, "admin");
verify(repository).lockBindings(22L);
verify(repository).confirm(31L, 0, 9L, "admin", "ok");
verify(repository).confirm(32L, 0, 9L, "admin", "ok");
verify(repository, times(1)).history(argThat(value -> "CONFIRM_DISPATCH".equals(value.getAction())
&& value.getContainerBookingId().equals(22L)));
verify(carrierSchedules, times(1)).ensureFromOutboundConfirm(7L, 66L, 22L, 9L, "admin");
}
@Test
void confirmNeverWritesStateWhenAnyRealGateFails() {
OutboundDispatchService service = new OutboundDispatchService(repository,
List.of(passing("CUSTOMS_CLEARANCE"), passing("SAILING_SCHEDULE")));
stubReadyChain();
when(repository.pick(44L)).thenReturn(new OutboundDispatchReadMapper.IdStatus(55L, "LOADING"));
when(repository.claim(eq(9L), eq("CONFIRM_DISPATCH"), eq("confirm-2"), anyString())).thenReturn(true);
void confirmDoesNotWriteAnyRowWhenOneInboundGateFails() {
OutboundDispatchService service = serviceWithoutCarrier();
stubContainer();
when(repository.lockBindings(22L)).thenReturn(bindings());
when(repository.evidence(7L, 12L)).thenReturn(new OutboundDispatchReadMapper.Evidence(4, 1));
when(repository.claim(eq(9L), eq("CONFIRM_DISPATCH"), eq("blocked"), anyString())).thenReturn(true);
assertThatThrownBy(() -> service.confirm(9L, "admin", "confirm-2",
new ActionRequest(11L, 22L, 0, null))).isInstanceOf(BizException.class).hasMessageContaining("门禁");
assertThatThrownBy(() -> service.confirm(22L, 9L, "admin", "blocked", new ActionRequest(0, null)))
.isInstanceOf(BizException.class).hasMessageContaining("全部入库绑定");
verify(repository, never()).confirm(anyLong(), anyInt(), anyLong(), anyString(), any());
verify(repository, never()).history(any());
}
@Test
void differingTechnicalRowVersionsAreRejectedAsContainerConflict() {
OutboundDispatchService service = serviceWithoutCarrier();
stubContainer();
when(repository.lockBindings(22L)).thenReturn(bindings());
when(repository.dispatchLocked(7L, 11L, 22L)).thenReturn(state(31L, 11L, "READY_FOR_DISPATCH", 0));
when(repository.dispatchLocked(7L, 12L, 22L)).thenReturn(state(32L, 12L, "READY_FOR_DISPATCH", 1));
when(repository.claim(eq(9L), eq("CONFIRM_DISPATCH"), eq("mixed"), anyString())).thenReturn(true);
assertThatThrownBy(() -> service.confirm(22L, 9L, "admin", "mixed", new ActionRequest(0, null)))
.isInstanceOf(BizException.class).hasMessageContaining("版本不一致");
verify(repository, never()).confirm(anyLong(), anyInt(), anyLong(), anyString(), any());
}
@Test
void reverseIsRejectedAfterActualDepartedWarehouseFactExists() {
void actualDepartedWarehouseFactBlocksContainerReverseBeforeAnyRowChanges() {
CarrierScheduleQueryService carrierSchedules = mock(CarrierScheduleQueryService.class);
OutboundDispatchService service = new OutboundDispatchService(repository, List.of(), carrierSchedules);
stubReadyChain();
OutboundDispatchEntity state = new OutboundDispatchEntity();
state.setId(33L); state.setTenantId(7L); state.setInboundId(11L); state.setContainerBookingId(22L);
state.setStatus("WAITING_FOR_LOADING"); state.setVersion(1);
when(repository.dispatch(7L, 11L, 22L)).thenReturn(state);
OutboundDispatchService service = service(carrierSchedules);
stubContainer();
when(repository.lockBindings(22L)).thenReturn(bindings());
when(repository.dispatchLocked(7L, 11L, 22L)).thenReturn(state(31L, 11L, "WAITING_FOR_LOADING", 1));
when(repository.dispatchLocked(7L, 12L, 22L)).thenReturn(state(32L, 12L, "WAITING_FOR_LOADING", 1));
when(repository.claim(eq(9L), eq("REVERSE_DISPATCH"), eq("reverse-blocked"), anyString())).thenReturn(true);
when(carrierSchedules.hasOutboundDepartureFact(7L, 66L, 22L)).thenReturn(true);
assertThatThrownBy(() -> service.reverse(9L, "admin", "reverse-blocked",
new ActionRequest(11L, 22L, 1, "误操作")))
.isInstanceOf(BizException.class).hasMessageContaining("DEPARTED_WAREHOUSE");
assertThatThrownBy(() -> service.reverse(22L, 9L, "admin", "reverse-blocked",
new ActionRequest(1, "误操作"))).isInstanceOf(BizException.class)
.hasMessageContaining("DEPARTED_WAREHOUSE");
verify(repository, never()).reverse(anyLong(), anyInt(), anyLong(), anyString(), any());
}
@Test
void selectorsThatMatchMultipleContainerBindingsRequireDisambiguation() {
OutboundDispatchService service = new OutboundDispatchService(repository, List.of());
when(repository.bindings(any())).thenReturn(List.of(binding(), binding()));
assertThatThrownBy(() -> service.detail(new Query(10L, null, null)))
.isInstanceOf(BizException.class).hasMessageContaining("多个出仓聚合");
void sameIdempotencyKeyAndPayloadReplaysContainerResultWithoutMutation() {
OutboundDispatchService service = serviceWithoutCarrier();
stubContainer();
AtomicReference<String> requestHash = new AtomicReference<>();
when(repository.claim(eq(9L), eq("CONFIRM_DISPATCH"), eq("replay"), anyString())).thenAnswer(invocation -> {
requestHash.set(invocation.getArgument(3));
return false;
});
OutboundDispatchOperationEntity existing = new OutboundDispatchOperationEntity();
existing.setStatus("SUCCESS");
when(repository.operation(9L, "CONFIRM_DISPATCH", "replay")).thenAnswer(invocation -> {
existing.setRequestHash(requestHash.get());
return existing;
});
Detail replay = service.confirm(22L, 9L, "admin", "replay", new ActionRequest(0, null));
assertThat(replay.aggregate().containerBookingId()).isEqualTo(22L);
verify(repository, never()).confirm(anyLong(), anyInt(), anyLong(), anyString(), any());
}
@Test
void staleDispatchVersionIsRejectedWithoutPartialInboundAdvance() {
OutboundDispatchService service = new OutboundDispatchService(repository,
List.of(passing("CUSTOMS_CLEARANCE"), passing("SAILING_SCHEDULE")));
stubReadyChain();
OutboundDispatchEntity state = new OutboundDispatchEntity();
state.setId(33L); state.setTenantId(7L); state.setInboundId(11L); state.setContainerBookingId(22L);
state.setStatus("READY_FOR_DISPATCH"); state.setVersion(2);
when(repository.dispatch(7L, 11L, 22L)).thenReturn(state);
when(repository.claim(eq(9L), eq("CONFIRM_DISPATCH"), eq("stale"), anyString())).thenReturn(true);
assertThatThrownBy(() -> service.confirm(9L, "admin", "stale",
new ActionRequest(11L, 22L, 1, null))).isInstanceOf(BizException.class).hasMessageContaining("刷新");
}
@Test
void reusedIdempotencyKeyWithDifferentHashIsRejected() {
OutboundDispatchService service = new OutboundDispatchService(repository,
List.of(passing("CUSTOMS_CLEARANCE"), passing("SAILING_SCHEDULE")));
stubReadyChain();
void reusedIdempotencyKeyWithDifferentPayloadIsRejected() {
OutboundDispatchService service = serviceWithoutCarrier();
when(repository.claim(eq(9L), eq("CONFIRM_DISPATCH"), eq("reused"), anyString())).thenReturn(false);
OutboundDispatchOperationEntity existing = new OutboundDispatchOperationEntity();
existing.setRequestHash("different"); existing.setStatus("SUCCESS");
existing.setRequestHash("different");
existing.setStatus("SUCCESS");
when(repository.operation(9L, "CONFIRM_DISPATCH", "reused")).thenReturn(existing);
assertThatThrownBy(() -> service.confirm(9L, "admin", "reused",
new ActionRequest(11L, 22L, 0, null))).isInstanceOf(BizException.class)
.hasMessageContaining("用于不同请求");
assertThatThrownBy(() -> service.confirm(22L, 9L, "admin", "reused", new ActionRequest(0, null)))
.isInstanceOf(BizException.class).hasMessageContaining("用于不同请求");
}
private void stubReadyChain() {
when(repository.bindings(any())).thenReturn(List.of(binding()));
private OutboundDispatchService serviceWithoutCarrier() {
return service(null);
}
private OutboundDispatchService service(CarrierScheduleQueryService carrierSchedules) {
return new OutboundDispatchService(repository,
List.of(passing("CUSTOMS_CLEARANCE"), passing("SAILING_SCHEDULE")), carrierSchedules);
}
private void stubContainer() {
when(repository.bindings(22L)).thenReturn(bindings());
when(repository.pick(44L)).thenReturn(new OutboundDispatchReadMapper.IdStatus(55L, "COMPLETED"));
when(repository.ocean(22L, 7L)).thenReturn(new OutboundDispatchReadMapper.IdStatus(66L, "RELEASED"));
when(repository.truck(22L, 44L, 7L)).thenReturn(new OutboundDispatchReadMapper.IdStatus(77L, "DEPARTED"));
when(repository.ocean(22L)).thenReturn(new OutboundDispatchReadMapper.OceanBinding(66L, "RELEASED", 7L));
when(repository.truck(22L, 44L)).thenReturn(new OutboundDispatchReadMapper.IdStatus(77L, "DEPARTED"));
when(repository.evidence(7L, 11L)).thenReturn(new OutboundDispatchReadMapper.Evidence(5, 1));
when(repository.evidence(7L, 12L)).thenReturn(new OutboundDispatchReadMapper.Evidence(5, 1));
}
private static OutboundDispatchReadMapper.Binding binding() {
return new OutboundDispatchReadMapper.Binding(10L, "O-10", 11L, "I-11", 7L, 8L, "客户A",
private static List<OutboundDispatchReadMapper.Binding> bindings() {
return List.of(binding(11L, "I-11"), binding(12L, "I-12"));
}
private static OutboundDispatchReadMapper.Binding binding(long inboundId, String inboundCode) {
return new OutboundDispatchReadMapper.Binding(10L, "O-10", inboundId, inboundCode, 7L, 8L, "客户A",
"LOADED", 22L, "CAB-22", "C-22", 44L, "COMPLETED");
}
private static OutboundDispatchEntity state(long id, long inboundId, String status, int version) {
OutboundDispatchEntity value = new OutboundDispatchEntity();
value.setId(id);
value.setTenantId(7L);
value.setOrderId(10L);
value.setInboundId(inboundId);
value.setContainerBookingId(22L);
value.setStatus(status);
value.setVersion(version);
return value;
}
private static OutboundDispatchGateContributor passing(String code) {
return new OutboundDispatchGateContributor() {
@Override public String code() { return code; }
@@ -180,4 +214,5 @@ class OutboundDispatchServiceTest {
}
};
}
}
@@ -38,17 +38,19 @@ class OutboundDispatchSqlContractTest {
}
@Test
void openApiPublishesSelectorsGateAndBothVersionedActions() throws Exception {
void openApiPublishesOnlyContainerLevelGateDetailAndVersionedActions() throws Exception {
String openApi = resource("openapi/phase3-outbound-dispatch-v1.yaml");
assertThat(openApi)
.contains("outbound-dispatches:")
.contains("outbound-dispatches/detail:")
.contains("outbound-dispatches/check-gate:")
.contains("outbound-dispatches/confirm-dispatch:")
.contains("outbound-dispatches/reverse-dispatch:")
.contains("outbound-dispatches/{containerBookingId}:")
.contains("outbound-dispatches/{containerBookingId}/gate:")
.contains("outbound-dispatches/{containerBookingId}/confirm:")
.contains("outbound-dispatches/{containerBookingId}/reverse:")
.contains("every real inbound binding", "No partial success is possible")
.contains("Once an actual DEPARTED_WAREHOUSE node exists")
.contains("expectedVersion")
.contains("Idempotency-Key");
.contains("Idempotency-Key")
.doesNotContain("confirm-dispatch", "reverse-dispatch", "check-gate", "inboundId, containerBookingId");
}
private static String migration() throws Exception { return resource("db/phase3-outbound-dispatch-schema.sql"); }
@@ -0,0 +1,65 @@
package com.baiyue.firstleg.outbound.dispatch;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.firstleg.outbound.dispatch.controller.PlatformOutboundDispatchController;
import com.baiyue.firstleg.outbound.dispatch.dto.OutboundDispatchDtos.ActionRequest;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class PlatformOutboundDispatchControllerContractTest {
@Test
void exposesOnlyContainerLevelResourcePathsAndExistingPermissions() {
assertThat(PlatformOutboundDispatchController.class.getAnnotation(RequestMapping.class).value())
.containsExactly("/api/platform/v1/first-leg/outbound-dispatches");
Map<String, String> permissions = Map.of(
"list", "platform:firstleg:outbound-dispatch:list",
"detail", "platform:firstleg:outbound-dispatch:view",
"checkGate", "platform:firstleg:outbound-dispatch:check",
"confirm", "platform:firstleg:outbound-dispatch:confirm",
"reverse", "platform:firstleg:outbound-dispatch:reverse");
permissions.forEach((name, permission) -> assertThat(method(name).getAnnotation(PreAuthorize.class).value())
.isEqualTo("hasAuthority('" + permission + "')"));
assertThat(method("detail").getAnnotation(GetMapping.class).value()).containsExactly("/{containerBookingId}");
assertThat(method("checkGate").getAnnotation(GetMapping.class).value())
.containsExactly("/{containerBookingId}/gate");
assertThat(method("confirm").getAnnotation(PostMapping.class).value())
.containsExactly("/{containerBookingId}/confirm");
assertThat(method("reverse").getAnnotation(PostMapping.class).value())
.containsExactly("/{containerBookingId}/reverse");
}
@Test
void actionBodyContainsOnlyContainerVersionAndRemarkAndAuditsContainerRoot() {
assertThat(Arrays.stream(ActionRequest.class.getRecordComponents()).map(value -> value.getName()))
.containsExactly("expectedVersion", "remark")
.doesNotContain("inboundId", "containerBookingId");
for (String name : List.of("confirm", "reverse")) {
BusinessAudit audit = method(name).getAnnotation(BusinessAudit.class);
assertThat(audit.resourceType()).isEqualTo("CONTAINER_BOOKING");
assertThat(audit.resourceId()).isEqualTo("#containerBookingId");
}
}
@Test
void everyNonListOperationUsesOnlyContainerBookingIdPathTarget() {
for (String name : List.of("detail", "checkGate", "confirm", "reverse")) {
assertThat(Arrays.stream(method(name).getParameters())
.filter(value -> value.isAnnotationPresent(PathVariable.class))
.map(value -> value.getName())).containsExactly("containerBookingId");
}
}
private static Method method(String name) {
return Arrays.stream(PlatformOutboundDispatchController.class.getDeclaredMethods())
.filter(value -> value.getName().equals(name)).findFirst().orElseThrow();
}
}