feat: enforce canonical container identity across regions
This commit is contained in:
@@ -13,6 +13,11 @@
|
||||
<name>integration-service</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.baiyue.shared</groupId>
|
||||
<artifactId>common-event-contract</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baiyue.shared</groupId>
|
||||
<artifactId>baiyue-common-core</artifactId>
|
||||
|
||||
+14
-3
@@ -1,5 +1,6 @@
|
||||
package com.baiyue.integration.service;
|
||||
|
||||
import com.baiyue.common.event.ContainerNumbers;
|
||||
import com.baiyue.integration.dto.CarrierScheduleDtos.QueryRequest;
|
||||
import com.baiyue.integration.dto.CarrierScheduleDtos.QueryResult;
|
||||
import com.baiyue.integration.provider.CarrierScheduleProviderAdapter;
|
||||
@@ -21,10 +22,20 @@ public class CarrierScheduleProviderService {
|
||||
}
|
||||
|
||||
public QueryResult query(QueryRequest request) {
|
||||
QueryResult result = adapters.stream().filter(adapter -> adapter.supports(request.carrierCode())).findFirst()
|
||||
if (!"CONTAINER_NO".equals(request.trackingType())) {
|
||||
throw new IllegalArgumentException("trackingType仅支持CONTAINER_NO");
|
||||
}
|
||||
String containerNo;
|
||||
try {
|
||||
containerNo = ContainerNumbers.requireCanonical(request.trackingNo());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new IllegalArgumentException("trackingNo必须是规范化真实集装箱号");
|
||||
}
|
||||
QueryRequest normalized = new QueryRequest(request.carrierCode(), "CONTAINER_NO", containerNo);
|
||||
QueryResult result = adapters.stream().filter(adapter -> adapter.supports(normalized.carrierCode())).findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException(
|
||||
"没有支持该船司的轨迹供应商适配器:" + request.carrierCode()))
|
||||
.query(request);
|
||||
"没有支持该船司的轨迹供应商适配器:" + normalized.carrierCode()))
|
||||
.query(normalized);
|
||||
if (result == null || result.observations() == null || result.exceptions() == null
|
||||
|| !OUTCOMES.contains(result.outcome())) {
|
||||
throw new IllegalStateException("轨迹供应商返回空结果");
|
||||
|
||||
@@ -26,8 +26,8 @@ components:
|
||||
required: [carrierCode, trackingType, trackingNo]
|
||||
properties:
|
||||
carrierCode: {type: string, enum: [MATSON, SML, ZIM, COSCO, EMC, HEDE]}
|
||||
trackingType: {type: string, enum: [BOOKING_NO, BILL_OF_LADING, CONTAINER_NO]}
|
||||
trackingNo: {type: string, minLength: 1, maxLength: 128}
|
||||
trackingType: {type: string, enum: [CONTAINER_NO]}
|
||||
trackingNo: {type: string, minLength: 1, maxLength: 128, description: Uppercase normalized physical containerNo supplied by Firstleg; no cabinet/order/booking/BOL fallback.}
|
||||
StandardNode:
|
||||
type: string
|
||||
enum: [DEPARTED_WAREHOUSE, AT_ORIGIN_PORT, LOADED_ON_VESSEL, DEPARTED_ORIGIN_PORT, IN_TRANSIT, ARRIVED_DESTINATION_PORT, DISCHARGED_FROM_VESSEL, PICKUP_READY, PICKED_UP, EMPTY_RETURNED]
|
||||
|
||||
+23
-3
@@ -18,13 +18,33 @@ class CarrierScheduleProviderServiceTest {
|
||||
@Test
|
||||
void fallbackNeverPretendsThatWebsiteScrapingSucceeded() {
|
||||
var service = new CarrierScheduleProviderService(List.of(new CompliantCarrierScheduleProviderAdapter()));
|
||||
QueryResult result = service.query(new QueryRequest("COSCO", "CONTAINER_NO", "CSNU1234567"));
|
||||
QueryResult result = service.query(new QueryRequest("COSCO", "CONTAINER_NO", "ZCSU7881970"));
|
||||
assertThat(result.outcome()).isEqualTo("MANUAL_REQUIRED");
|
||||
assertThat(result.errorCode()).isEqualTo("PROVIDER_API_NOT_CONFIGURED");
|
||||
assertThat(result.observations()).isEmpty();
|
||||
assertThat(result.exceptions()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlyDispatchesNormalizedContainerNumberTracking() {
|
||||
CarrierScheduleProviderAdapter capturing = new CarrierScheduleProviderAdapter() {
|
||||
@Override public boolean supports(String carrierCode) { return true; }
|
||||
@Override public QueryResult query(QueryRequest request) {
|
||||
assertThat(request.trackingType()).isEqualTo("CONTAINER_NO");
|
||||
assertThat(request.trackingNo()).isEqualTo("ZCSU7881970");
|
||||
return new QueryResult("TEST", "NO_CHANGE", List.of(), List.of(), null, null, LocalDateTime.now());
|
||||
}
|
||||
};
|
||||
var service = new CarrierScheduleProviderService(List.of(capturing));
|
||||
service.query(new QueryRequest("ZIM", "CONTAINER_NO", "ZCSU7881970"));
|
||||
assertThatThrownBy(() -> service.query(new QueryRequest("ZIM", "CONTAINER_NO", "zcsu7881970")))
|
||||
.hasMessageContaining("规范化真实集装箱号");
|
||||
assertThatThrownBy(() -> service.query(new QueryRequest("ZIM", "CONTAINER_NO", "ZCSU7881971")))
|
||||
.hasMessageContaining("规范化真实集装箱号");
|
||||
assertThatThrownBy(() -> service.query(new QueryRequest("ZIM", "BILL_OF_LADING", "BL-1")))
|
||||
.hasMessageContaining("CONTAINER_NO");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsProviderSpecificNodesThatWereNotNormalized() {
|
||||
CarrierScheduleProviderAdapter invalid = new CarrierScheduleProviderAdapter() {
|
||||
@@ -37,7 +57,7 @@ class CarrierScheduleProviderServiceTest {
|
||||
}
|
||||
};
|
||||
var service = new CarrierScheduleProviderService(List.of(invalid));
|
||||
assertThatThrownBy(() -> service.query(new QueryRequest("COSCO", "CONTAINER_NO", "CSNU1234567")))
|
||||
assertThatThrownBy(() -> service.query(new QueryRequest("COSCO", "CONTAINER_NO", "ZCSU7881970")))
|
||||
.hasMessageContaining("无效标准节点");
|
||||
}
|
||||
|
||||
@@ -53,7 +73,7 @@ class CarrierScheduleProviderServiceTest {
|
||||
}
|
||||
};
|
||||
var service = new CarrierScheduleProviderService(List.of(invalid));
|
||||
assertThatThrownBy(() -> service.query(new QueryRequest("COSCO", "CONTAINER_NO", "CSNU1234567")))
|
||||
assertThatThrownBy(() -> service.query(new QueryRequest("COSCO", "CONTAINER_NO", "ZCSU7881970")))
|
||||
.hasMessageContaining("无效异常事实");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user