feat(orders): expose customer-owned loaded transport facts
This commit is contained in:
@@ -48,7 +48,11 @@ public interface FirstlegUnifiedOrderClient {
|
||||
|
||||
record Summary(Inbound inbound, String mainStatus, String nextAction, Boolean customerFeeConfirmationRequired,
|
||||
Preload preload, Loading loading, Delivery delivery) {}
|
||||
record Detail(Summary summary, List<Timeline> timeline) {}
|
||||
record Detail(Summary summary, List<Timeline> timeline, List<TransportFragment> transportFragments) {}
|
||||
record TransportFragment(String inboundCode, String cabinetNo, String containerNo, List<TransportNode> nodes) {}
|
||||
record TransportNode(String standardNode, Integer nodeSequence, String providerCode,
|
||||
LocalDateTime estimatedAt, LocalDateTime actualAt,
|
||||
String locationCode, String locationName) {}
|
||||
record Inbound(Long id, Long tenantId, Long orderId, String orderNo, String inboundCode,
|
||||
String processStatus, String orderStatusSnapshot, String measureReviewStatus,
|
||||
Integer version,
|
||||
|
||||
@@ -48,7 +48,13 @@ public final class UnifiedOrderResponse {
|
||||
List<String> memberInboundCodes) {}
|
||||
|
||||
public record Detail(Row summary, OrderDetailResponse order,
|
||||
List<TimelineEvent> timeline) {}
|
||||
List<TimelineEvent> timeline,
|
||||
List<TransportFragment> transportFragments) {}
|
||||
public record TransportFragment(String inboundCode, String cabinetNo, String containerNo,
|
||||
List<TransportNode> nodes) {}
|
||||
public record TransportNode(String standardNode, Integer nodeSequence, String providerCode,
|
||||
LocalDateTime estimatedAt, LocalDateTime actualAt,
|
||||
String locationCode, String locationName) {}
|
||||
|
||||
public record MeasurementSummary(String reviewStatus, BigDecimal actualGrossWeightKg,
|
||||
BigDecimal actualVolumeCbm, BigDecimal chargeableWeightKg,
|
||||
|
||||
@@ -153,7 +153,13 @@ public class UnifiedOrderQueryService {
|
||||
}
|
||||
timeline.sort(Comparator.comparing(UnifiedOrderResponse.TimelineEvent::occurredAt,
|
||||
Comparator.nullsLast(Comparator.naturalOrder())));
|
||||
return new UnifiedOrderResponse.Detail(summary, orderDetail, timeline);
|
||||
var fragments = inbound == null ? List.<UnifiedOrderResponse.TransportFragment>of()
|
||||
: inbound.transportFragments().stream().map(fragment -> new UnifiedOrderResponse.TransportFragment(
|
||||
fragment.inboundCode(), fragment.cabinetNo(), fragment.containerNo(),
|
||||
fragment.nodes().stream().map(node -> new UnifiedOrderResponse.TransportNode(
|
||||
node.standardNode(), node.nodeSequence(), node.providerCode(), node.estimatedAt(),
|
||||
node.actualAt(), node.locationCode(), node.locationName())).toList())).toList();
|
||||
return new UnifiedOrderResponse.Detail(summary, orderDetail, timeline, fragments);
|
||||
}
|
||||
|
||||
private PageResult<UnifiedOrderResponse.Row> pageFromFirstleg(Long tenantId, UnifiedOrderQuery query,
|
||||
|
||||
@@ -910,6 +910,25 @@ components:
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
schemas:
|
||||
CustomerTransportFragment:
|
||||
type: object
|
||||
required: [nodes]
|
||||
properties:
|
||||
inboundCode: {type: string, nullable: true}
|
||||
cabinetNo: {type: string, nullable: true}
|
||||
containerNo: {type: string, nullable: true}
|
||||
nodes: {type: array, items: {$ref: '#/components/schemas/CustomerTransportNode'}}
|
||||
CustomerTransportNode:
|
||||
type: object
|
||||
properties:
|
||||
standardNode: {type: string, description: 标准船节点;进港为AT_ORIGIN_PORT,不表示开航}
|
||||
nodeSequence: {type: integer}
|
||||
providerCode: {type: string, nullable: true}
|
||||
estimatedAt: {type: string, format: date-time, nullable: true}
|
||||
actualAt: {type: string, format: date-time, nullable: true, description: 仅实际时间表示实际发生,不能用预计时间替代}
|
||||
locationCode: {type: string, nullable: true}
|
||||
locationName: {type: string, nullable: true}
|
||||
|
||||
CargoLabelBindingListEnvelope:
|
||||
type: object
|
||||
required: [code, data]
|
||||
@@ -1110,7 +1129,12 @@ components:
|
||||
data: {$ref: '#/components/schemas/UnifiedOrderDetail'}
|
||||
UnifiedOrderDetail:
|
||||
type: object
|
||||
required: [transportFragments]
|
||||
properties:
|
||||
transportFragments:
|
||||
type: array
|
||||
description: 仅当前客户当前订单已完成实装片段的柜级运输事实,按入仓码和柜次分别返回,不包含跳拣来源或其他客户
|
||||
items: {$ref: '#/components/schemas/CustomerTransportFragment'}
|
||||
summary: {$ref: '#/components/schemas/UnifiedOrderRow'}
|
||||
order:
|
||||
type: object
|
||||
|
||||
@@ -128,4 +128,19 @@ class UnifiedOrderApiContractTest {
|
||||
Map<String, Object> content = stringMap(response.get("content"));
|
||||
return stringMap(stringMap(content.get("application/json")).get("schema"));
|
||||
}
|
||||
@Test
|
||||
void transportFragmentsDocumentCustomerSafeFactsWithoutInternalIdentifiers() throws Exception {
|
||||
Map<String,Object> document=stringMap(new Yaml().load(new ClassPathResource("openapi/order-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8)));
|
||||
var schemas=stringMap(stringMap(document.get("components")).get("schemas"));
|
||||
var detail=stringMap(schemas.get("UnifiedOrderDetail"));
|
||||
assertThat(stringList(detail.get("required"))).contains("transportFragments");
|
||||
var fragments=stringMap(stringMap(detail.get("properties")).get("transportFragments"));
|
||||
assertThat(stringMap(fragments.get("items")).get("$ref")).isEqualTo("#/components/schemas/CustomerTransportFragment");
|
||||
assertThat(stringMap(stringMap(schemas.get("CustomerTransportFragment")).get("properties")))
|
||||
.containsOnlyKeys("inboundCode","cabinetNo","containerNo","nodes");
|
||||
assertThat(stringMap(stringMap(schemas.get("CustomerTransportNode")).get("properties")))
|
||||
.containsOnlyKeys("standardNode","nodeSequence","providerCode","estimatedAt","actualAt","locationCode","locationName");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -488,7 +488,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
when(orders.findNotes(10001L, 77L)).thenReturn(List.of());
|
||||
when(orders.findHistory(10001L, 77L)).thenReturn(List.of());
|
||||
FirstlegUnifiedOrderClient.Detail fulfillment =
|
||||
new FirstlegUnifiedOrderClient.Detail(summary("LOADING", null), List.of());
|
||||
new FirstlegUnifiedOrderClient.Detail(summary("LOADING", null), List.of(), List.of());
|
||||
when(firstleg.detail(eq("internal-token"), any())).thenReturn(ApiResponse.success(fulfillment));
|
||||
var group = new FirstlegColoadGroupClient.GroupSnapshot(
|
||||
700L, "CLG-700", "LOCKED", 11L, "MATSON", 900L, 1,
|
||||
@@ -710,7 +710,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
null, 9L, "平台操作员", LocalDateTime.of(2026, 9, 8, 10, 0));
|
||||
var summary = summary("WAIT_PALLETIZE", "PALLETIZE_DROPSHIP");
|
||||
when(firstleg.detail(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new FirstlegUnifiedOrderClient.Detail(summary, List.of(timeline))));
|
||||
new FirstlegUnifiedOrderClient.Detail(summary, List.of(timeline), List.of())));
|
||||
|
||||
var result = service.detail(10001L, 77L, false);
|
||||
|
||||
@@ -761,12 +761,23 @@ class UnifiedOrderQueryServiceTest {
|
||||
when(orders.findParties(10001L, 77L)).thenReturn(List.of());
|
||||
when(orders.findNotes(10001L, 77L)).thenReturn(List.of());
|
||||
when(orders.findHistory(10001L, 77L)).thenReturn(List.of());
|
||||
var fulfillmentDetail = new FirstlegUnifiedOrderClient.Detail(summary("LOADED", null), List.of());
|
||||
var portEntry = new FirstlegUnifiedOrderClient.TransportNode("AT_ORIGIN_PORT",20,"MANUAL",
|
||||
null,LocalDateTime.of(2026,10,5,9,0),"CNSZX","深圳");
|
||||
var estimatedSailing = new FirstlegUnifiedOrderClient.TransportNode("DEPARTED_ORIGIN_PORT",40,"MATSON",
|
||||
LocalDateTime.of(2026,10,7,8,0),null,null,null);
|
||||
var fulfillmentDetail = new FirstlegUnifiedOrderClient.Detail(summary("LOADED", null), List.of(),
|
||||
List.of(new FirstlegUnifiedOrderClient.TransportFragment("NY-1","D1","TSTU0000001",List.of(portEntry)),
|
||||
new FirstlegUnifiedOrderClient.TransportFragment("NY-2","D2","TSTU0000002",List.of(estimatedSailing))));
|
||||
when(firstleg.detail(eq("internal-token"), any()))
|
||||
.thenReturn(ApiResponse.success(fulfillmentDetail));
|
||||
|
||||
var result = service.detail(10001L, 77L, false);
|
||||
|
||||
assertThat(result.transportFragments()).hasSize(2);
|
||||
assertThat(result.transportFragments().get(0).nodes().get(0).standardNode()).isEqualTo("AT_ORIGIN_PORT");
|
||||
assertThat(result.transportFragments().get(0).nodes().get(0).actualAt()).isNotNull();
|
||||
assertThat(result.transportFragments().get(1).nodes().get(0).actualAt()).isNull();
|
||||
assertThat(result.transportFragments().get(1).nodes().get(0).estimatedAt()).isNotNull();
|
||||
assertThat(result.summary().mainStatus()).isEqualTo("LOADED");
|
||||
assertThat(result.summary().internalOrderStatus()).isEqualTo(OrderStatus.APPROVED);
|
||||
assertThat(result.summary().inboundId()).isEqualTo(88L);
|
||||
@@ -887,7 +898,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
when(orders.findNotes(10001L, 77L)).thenReturn(List.of());
|
||||
when(orders.findHistory(10001L, 77L)).thenReturn(List.of());
|
||||
when(firstleg.detail(eq("internal-token"), any())).thenReturn(
|
||||
ApiResponse.success(new FirstlegUnifiedOrderClient.Detail(null, List.of())));
|
||||
ApiResponse.success(new FirstlegUnifiedOrderClient.Detail(null, List.of(), List.of())));
|
||||
|
||||
assertServiceUnavailable(() -> service.detail(10001L, 77L, false));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user