fix(firstleg): validate booking carrier associations
This commit is contained in:
@@ -34,7 +34,7 @@ class CustomsServiceTest {
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||
ReflectionTestUtils.setField(service, "internalToken", "token");
|
||||
when(suppliers.customs(anyLong(), anyString()))
|
||||
.thenReturn(new com.baiyue.firstleg.client.WarehouseSupplierClient.Snapshot(88L, "线下报关代理", "CUSTOMS", "ACTIVE"));
|
||||
.thenReturn(new com.baiyue.firstleg.client.WarehouseSupplierClient.Snapshot(88L, "线下报关代理", "CUSTOMS", "ACTIVE", java.util.List.of()));
|
||||
when(repository.completeOperation(anyLong(), anyString(), anyLong())).thenReturn(1);
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -21,6 +21,7 @@ class PlatformOceanBookingControllerContractTest {
|
||||
Map<String, String> permissions = Map.ofEntries(
|
||||
Map.entry("page", "platform:firstleg:ocean-booking:list"),
|
||||
Map.entry("linkCandidates", "platform:firstleg:ocean-booking:list"),
|
||||
Map.entry("bookingAgents", "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"),
|
||||
@@ -31,7 +32,7 @@ class PlatformOceanBookingControllerContractTest {
|
||||
Method method = method(name);
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('" + permission + "')");
|
||||
if (!name.equals("page") && !name.equals("detail") && !name.equals("channels")
|
||||
if (!name.equals("page") && !name.equals("detail") && !name.equals("channels") && !name.equals("bookingAgents")
|
||||
&& !name.equals("linkCandidates")) {
|
||||
assertThat(method.getAnnotation(BusinessAudit.class)).isNotNull();
|
||||
}
|
||||
@@ -94,6 +95,10 @@ class PlatformOceanBookingControllerContractTest {
|
||||
.contains("enum: [MATSON, SML, ZIM, COSCO, EMC, HEDE, OTHER]")
|
||||
.contains("Required before RELEASE")
|
||||
.contains("OTHER means manual carrier-node tracking");
|
||||
assertThat(yaml)
|
||||
.contains("/api/platform/v1/ocean-bookings/booking-agents:")
|
||||
.contains("Filtering is enforced by Warehouse master data")
|
||||
.contains("BookingAgentOption:");
|
||||
}
|
||||
|
||||
private static Method method(String name) {
|
||||
|
||||
+51
-1
@@ -37,6 +37,7 @@ class OceanBookingServiceTest {
|
||||
@Mock private PriceBookQueryUseCase priceBooks;
|
||||
@Mock private WarehouseSupplierSnapshotService suppliers;
|
||||
@Mock private WarehouseMasterDataClient warehouses;
|
||||
@Mock private com.baiyue.firstleg.client.WarehousePartnerClient partners;
|
||||
@Mock private FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@InjectMocks private OceanBookingService service;
|
||||
private ObjectMapper objectMapper;
|
||||
@@ -116,6 +117,7 @@ class OceanBookingServiceTest {
|
||||
|
||||
@Test
|
||||
void releasesCompleteAggregateAndReplaysFrozenResult() throws Exception {
|
||||
stubMasterData();
|
||||
OceanBookingEntity draft = completeBooking("DRAFT", 3);
|
||||
OceanBookingEntity released = completeBooking("RELEASED", 4);
|
||||
when(repository.find(1L)).thenReturn(draft, released);
|
||||
@@ -139,6 +141,51 @@ class OceanBookingServiceTest {
|
||||
verify(repository, times(1)).transition(1L, "DRAFT", "RELEASED", 3, 9L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsCarrierSnapshotMismatchAndUnassociatedBookingAgent() {
|
||||
stubMasterData();
|
||||
when(partners.active(any(), eq(0L), eq(71L))).thenReturn(ApiResponse.success(
|
||||
new com.baiyue.firstleg.client.WarehousePartnerClient.Snapshot(71L, 0L, "船公司", "CARRIER",
|
||||
"SML", List.of("CARRIER"), "ACTIVE")));
|
||||
assertThatThrownBy(() -> service.create(9L, "admin", "carrier-mismatch",
|
||||
new CreateRequest(0, completeDraft())))
|
||||
.isInstanceOf(BizException.class).hasMessage("船司标准编码与主数据快照不一致");
|
||||
|
||||
stubMasterData();
|
||||
when(suppliers.booking(81L, null)).thenReturn(
|
||||
new WarehouseSupplierClient.Snapshot(81L, "订舱代理", "BOOKING", "ACTIVE", List.of("SML")));
|
||||
assertThatThrownBy(() -> service.create(9L, "admin", "agent-mismatch",
|
||||
new CreateRequest(0, completeDraft())))
|
||||
.isInstanceOf(BizException.class).hasMessage("所选订舱代理未关联当前船公司");
|
||||
verify(repository, never()).insert(any(OceanBookingEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bookingAgentCandidatesAreFilteredByCarrierCode() {
|
||||
when(suppliers.bookingCandidates("MATSON")).thenReturn(List.of(
|
||||
new WarehouseSupplierClient.Snapshot(81L, "匹配代理", "BOOKING", "ACTIVE", List.of("MATSON")),
|
||||
new WarehouseSupplierClient.Snapshot(82L, "不匹配代理", "BOOKING", "ACTIVE", List.of("SML"))));
|
||||
|
||||
assertThat(service.bookingAgents("matson")).extracting(BookingAgentOption::name)
|
||||
.containsExactly("匹配代理");
|
||||
}
|
||||
|
||||
@Test
|
||||
void releaseRevalidatesCurrentCarrierAndBookingAgentAssociation() {
|
||||
stubMasterData();
|
||||
OceanBookingEntity draft = completeBooking("DRAFT", 3);
|
||||
when(repository.find(1L)).thenReturn(draft);
|
||||
when(repository.parties(1L)).thenReturn(parties());
|
||||
when(repository.requirements(1L)).thenReturn(List.of(requirement()));
|
||||
when(suppliers.booking(81L, null)).thenReturn(
|
||||
new WarehouseSupplierClient.Snapshot(81L, "订舱代理", "BOOKING", "ACTIVE", List.of("SML")));
|
||||
|
||||
assertThatThrownBy(() -> service.release(1L, 9L, "admin", "release-agent-mismatch",
|
||||
new ReleaseRequest(3, "确认")))
|
||||
.isInstanceOf(BizException.class).hasMessage("所选订舱代理未关联当前船公司");
|
||||
verify(repository, never()).transition(anyLong(), anyString(), anyString(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasedAggregateRejectsEditDeleteAndRepeatRelease() {
|
||||
when(repository.find(1L)).thenReturn(booking("RELEASED", 4));
|
||||
@@ -255,7 +302,10 @@ class OceanBookingServiceTest {
|
||||
new WarehouseMasterDataClient.Detail(new WarehouseMasterDataClient.Warehouse(
|
||||
701L, 0L, "SZX", "深圳中转仓", "DOMESTIC", "ACTIVE"))));
|
||||
when(suppliers.booking(81L, null)).thenReturn(
|
||||
new WarehouseSupplierClient.Snapshot(81L, "订舱代理", "BOOKING", "ACTIVE"));
|
||||
new WarehouseSupplierClient.Snapshot(81L, "订舱代理", "BOOKING", "ACTIVE", List.of("MATSON")));
|
||||
when(partners.active(any(), eq(0L), eq(71L))).thenReturn(ApiResponse.success(
|
||||
new com.baiyue.firstleg.client.WarehousePartnerClient.Snapshot(71L, 0L, "船公司", "CARRIER",
|
||||
"MATSON", List.of("CARRIER"), "ACTIVE")));
|
||||
}
|
||||
|
||||
private static DraftData partialDraft(String remark) {
|
||||
|
||||
+1
-1
@@ -217,7 +217,7 @@ class ContainerFeeDetailServiceTest {
|
||||
when(feeItems.findItem(11L)).thenReturn(item());
|
||||
when(suppliers.fee(71L, null)).thenReturn(
|
||||
new com.baiyue.firstleg.client.WarehouseSupplierClient.Snapshot(
|
||||
71L, "现有供应商", "TRUCKING", "ACTIVE"));
|
||||
71L, "现有供应商", "TRUCKING", "ACTIVE", List.of()));
|
||||
LocalDateTime paidAt = LocalDateTime.of(2026, 8, 20, 10, 0);
|
||||
var request = new ContainerFeeDetailDtos.SaveRequest(11L, "PAYABLE",
|
||||
LocalDate.of(2026, 8, 20), new BigDecimal("300.00"), "CNY", 71L,
|
||||
|
||||
@@ -65,7 +65,7 @@ class ContainerServiceTest {
|
||||
when(warehouses.active(anyString(), anyLong(), anyLong())).thenAnswer(invocation ->
|
||||
activeWarehouse(invocation.getArgument(2, Long.class), "DOMESTIC", "上海中转仓"));
|
||||
when(suppliers.trucking(202L, "拖车公司A"))
|
||||
.thenReturn(new WarehouseSupplierClient.Snapshot(202L, "拖车公司A", "TRUCKING", "ACTIVE"));
|
||||
.thenReturn(new WarehouseSupplierClient.Snapshot(202L, "拖车公司A", "TRUCKING", "ACTIVE", List.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -58,7 +58,7 @@ class TruckAppointmentServiceTest {
|
||||
new IamCustomerSnapshotClient.Customer(invocation.getArgument(0), "C-7",
|
||||
"Canonical Customer", "ACTIVE", null, null));
|
||||
when(suppliers.trucking(any(), any())).thenReturn(
|
||||
new WarehouseSupplierClient.Snapshot(8L, "Canonical Carrier", "TRUCKING", "ACTIVE"));
|
||||
new WarehouseSupplierClient.Snapshot(8L, "Canonical Carrier", "TRUCKING", "ACTIVE", java.util.List.of()));
|
||||
lenient().when(repository.attachments(anyLong())).thenReturn(List.of());
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -36,7 +36,7 @@ class WarehouseSupplierSnapshotServiceTest {
|
||||
@Test
|
||||
void resolvesActiveBookingSupplierByCategoryWithoutScopeAndUsesServerName() {
|
||||
when(client.get("internal-token", 7L, true, "BOOKING"))
|
||||
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(7L, "海运代理", "BOOKING", "ACTIVE")));
|
||||
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(7L, "海运代理", "BOOKING", "ACTIVE", java.util.List.of("MATSON"))));
|
||||
var snapshot = service.booking(7L, "海运代理");
|
||||
assertThat(snapshot.name()).isEqualTo("海运代理");
|
||||
verify(client).get("internal-token", 7L, true, "BOOKING");
|
||||
@@ -46,7 +46,7 @@ class WarehouseSupplierSnapshotServiceTest {
|
||||
void feeWithoutCategoryAcceptsAnyActiveWarehouseSupplier() {
|
||||
when(client.get("internal-token", 8L, true, null))
|
||||
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(
|
||||
8L, "任意启用供应商", "CERTIFICATE", "ACTIVE")));
|
||||
8L, "任意启用供应商", "CERTIFICATE", "ACTIVE", java.util.List.of())));
|
||||
|
||||
assertThat(service.fee(8L, null).name()).isEqualTo("任意启用供应商");
|
||||
verify(client).get("internal-token", 8L, true, null);
|
||||
@@ -55,7 +55,7 @@ class WarehouseSupplierSnapshotServiceTest {
|
||||
@Test
|
||||
void rejectsClientNameThatDiffersFromWarehouseSnapshot() {
|
||||
when(client.get(anyString(), eq(9L), eq(true), eq("CUSTOMS")))
|
||||
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(9L, "真实报关行", "CUSTOMS", "ACTIVE")));
|
||||
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(9L, "真实报关行", "CUSTOMS", "ACTIVE", java.util.List.of())));
|
||||
assertThatThrownBy(() -> service.customs(9L, "伪造名称"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不一致");
|
||||
}
|
||||
@@ -64,7 +64,7 @@ class WarehouseSupplierSnapshotServiceTest {
|
||||
void rejectsSupplierSnapshotWithWrongCategoryOrInactiveStatus() {
|
||||
when(client.get("internal-token", 7L, true, "BOOKING"))
|
||||
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(
|
||||
7L, "错误代理", "TRUCKING", "ACTIVE")));
|
||||
7L, "错误代理", "TRUCKING", "ACTIVE", java.util.List.of())));
|
||||
|
||||
assertThatThrownBy(() -> service.booking(7L, null))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("类别不匹配");
|
||||
|
||||
Reference in New Issue
Block a user