feat(orders): expose customer-owned loaded transport facts
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** Customer-safe, read-only transport facts for one actually loaded inbound/container fragment. */
|
||||
public record CustomerTransportFragment(String inboundCode, String cabinetNo, String containerNo,
|
||||
List<Node> nodes) {
|
||||
public record Node(String standardNode, Integer nodeSequence, String providerCode,
|
||||
LocalDateTime estimatedAt, LocalDateTime actualAt,
|
||||
String locationCode, String locationName) {}
|
||||
|
||||
public record Row(Long inboundId, Long containerBookingId, String inboundCode, String cabinetNo,
|
||||
String containerNo, String standardNode, Integer nodeSequence, String providerCode,
|
||||
LocalDateTime estimatedAt, LocalDateTime actualAt,
|
||||
String locationCode, String locationName) {}
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import java.util.List;
|
||||
/** Detail payload for the product-level order detail. */
|
||||
public record UnifiedInboundDetail(
|
||||
UnifiedInboundSummary summary,
|
||||
List<TimelineEvent> timeline
|
||||
List<TimelineEvent> timeline,
|
||||
List<CustomerTransportFragment> transportFragments
|
||||
) {
|
||||
public record TimelineEvent(String source, String eventType, String fromStatus, String toStatus,
|
||||
String action, String remark, Long operatorId, String operatorName,
|
||||
|
||||
@@ -10,6 +10,29 @@ import java.util.List;
|
||||
/** 统一订单按当前有效装柜明细批量读取柜次船期。 */
|
||||
@Mapper
|
||||
public interface UnifiedInboundTransportMapper {
|
||||
@Select("""
|
||||
SELECT owned.inbound_id, owned.container_booking_id, owned.inbound_code,
|
||||
b.cabinet_no, b.container_no, n.standard_node, n.node_sequence, n.provider_code,
|
||||
n.estimated_at, n.actual_at, n.location_code, n.location_name
|
||||
FROM (
|
||||
SELECT DISTINCT o.id AS inbound_id, p.container_booking_id, o.inbound_code
|
||||
FROM fl_inbound_order o
|
||||
JOIN fl_loading_plan_item i ON i.inbound_id=o.id AND i.account_id=o.tenant_id
|
||||
JOIN fl_loading_plan p ON p.id=i.loading_plan_id AND p.plan_status='COMPLETED'
|
||||
JOIN fl_loading_pick_task t ON t.loading_plan_id=p.id AND t.task_status='COMPLETED'
|
||||
JOIN fl_loading_pick_item picked ON picked.pick_task_id=t.id
|
||||
AND picked.loading_plan_item_id=i.id
|
||||
WHERE o.tenant_id=#{tenantId} AND o.order_id=#{orderId} AND o.deleted=0
|
||||
AND i.deleted=0 AND picked.item_status IN ('PICKED','PARTIAL')
|
||||
AND picked.picked_package_count>0
|
||||
) owned
|
||||
JOIN fl_container_booking b ON b.id=owned.container_booking_id
|
||||
LEFT JOIN fl_carrier_schedule_node_fact n ON n.container_booking_id=owned.container_booking_id
|
||||
ORDER BY owned.inbound_id, owned.container_booking_id, n.node_sequence, n.id
|
||||
""")
|
||||
List<com.baiyue.firstleg.dto.CustomerTransportFragment.Row> findCustomerTransportNodes(
|
||||
@Param("tenantId") long tenantId, @Param("orderId") long orderId);
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT DISTINCT i.inbound_id, i.account_id, p.container_booking_id,
|
||||
|
||||
+18
@@ -86,6 +86,24 @@ public class UnifiedInboundReadRepository {
|
||||
return findTransports(Map.of(inboundId, tenantId)).get(inboundId);
|
||||
}
|
||||
|
||||
public List<com.baiyue.firstleg.dto.CustomerTransportFragment> findCustomerTransportFragments(
|
||||
long tenantId, long orderId) {
|
||||
var grouped = new java.util.LinkedHashMap<String,
|
||||
List<com.baiyue.firstleg.dto.CustomerTransportFragment.Row>>();
|
||||
transports.findCustomerTransportNodes(tenantId, orderId).forEach(row ->
|
||||
grouped.computeIfAbsent(row.inboundId() + ":" + row.containerBookingId(),
|
||||
ignored -> new java.util.ArrayList<>()).add(row));
|
||||
return grouped.values().stream().map(rows -> {
|
||||
var first = rows.get(0);
|
||||
var nodes = rows.stream().filter(row -> row.standardNode() != null)
|
||||
.map(row -> new com.baiyue.firstleg.dto.CustomerTransportFragment.Node(
|
||||
row.standardNode(), row.nodeSequence(), row.providerCode(), row.estimatedAt(),
|
||||
row.actualAt(), row.locationCode(), row.locationName())).toList();
|
||||
return new com.baiyue.firstleg.dto.CustomerTransportFragment(first.inboundCode(),
|
||||
first.cabinetNo(), first.containerNo(), nodes);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
public Map<Long, CurrentContainerTransport> findTransports(Map<Long, Long> tenantByInbound) {
|
||||
if (tenantByInbound.isEmpty()) return Map.of();
|
||||
Map<Long, List<CurrentContainerTransport>> grouped = transports
|
||||
|
||||
@@ -83,8 +83,11 @@ public class UnifiedInboundQueryService {
|
||||
? operations.findTransport(tenantId, inbound.getId()) : null;
|
||||
boolean customerFeeConfirmationRequired = pricing.customerFeeConfirmationRequiredMap(
|
||||
tenantId, Map.of(inbound.getId(), inbound.getTenantId())).getOrDefault(inbound.getId(), false);
|
||||
boolean hasTransportScope = includePhaseThree || inbounds.findByOrderIds(tenantId, List.of(orderId))
|
||||
.stream().anyMatch(item -> requiresPhaseThreeSummary(mainStatus(item.getProcessStatus())));
|
||||
return new UnifiedInboundDetail(summary(inbound, detail, preload, pick, evidence, deliveryAt,
|
||||
transport, null, null, customerFeeConfirmationRequired), timeline);
|
||||
transport, null, null, customerFeeConfirmationRequired), timeline,
|
||||
hasTransportScope ? operations.findCustomerTransportFragments(tenantId, orderId) : List.of());
|
||||
}
|
||||
|
||||
private List<UnifiedInboundSummary> summaries(List<InboundOrderEntity> records) {
|
||||
|
||||
@@ -1484,6 +1484,25 @@ components:
|
||||
required: true
|
||||
schema: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
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}
|
||||
|
||||
InboundHoldCommand:
|
||||
type: object
|
||||
required: [expectedVersion, reason]
|
||||
@@ -1660,7 +1679,12 @@ components:
|
||||
nullable: true
|
||||
UnifiedInboundDetail:
|
||||
type: object
|
||||
required: [transportFragments]
|
||||
properties:
|
||||
transportFragments:
|
||||
type: array
|
||||
description: 仅当前客户当前订单已完成实装片段的柜级运输事实,按入仓码和柜次分别返回,不包含跳拣来源或其他客户
|
||||
items: {$ref: '#/components/schemas/CustomerTransportFragment'}
|
||||
summary: {$ref: '#/components/schemas/UnifiedInboundSummary'}
|
||||
timeline:
|
||||
type: array
|
||||
|
||||
+15
@@ -59,4 +59,19 @@ class UnifiedInboundOpenApiContractTest {
|
||||
private static List<String> stringList(Object value) {
|
||||
return (List<String>) value;
|
||||
}
|
||||
@Test
|
||||
void transportFragmentsDocumentCustomerSafeFactsWithoutInternalIdentifiers() throws Exception {
|
||||
Map<String,Object> document=stringMap(new Yaml().load(new ClassPathResource("openapi/firstleg-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8)));
|
||||
var schemas=stringMap(stringMap(document.get("components")).get("schemas"));
|
||||
var detail=stringMap(schemas.get("UnifiedInboundDetail"));
|
||||
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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baiyue.firstleg.mapper.UnifiedInboundTransportMapper;
|
||||
import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class CustomerTransportFragmentPersistenceTest {
|
||||
private JdbcTemplate jdbc;
|
||||
private UnifiedInboundReadRepository repository;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
var ds = new JdbcDataSource();
|
||||
ds.setURL("jdbc:h2:mem:customer-transport-" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1");
|
||||
jdbc = new JdbcTemplate(ds);
|
||||
jdbc.execute("CREATE TABLE fl_inbound_order(id BIGINT,tenant_id BIGINT,order_id BIGINT,inbound_code VARCHAR,deleted INT)");
|
||||
jdbc.execute("CREATE TABLE fl_loading_plan_item(id BIGINT,loading_plan_id BIGINT,inbound_id BIGINT,account_id BIGINT,deleted INT)");
|
||||
jdbc.execute("CREATE TABLE fl_loading_plan(id BIGINT,container_booking_id BIGINT,plan_status VARCHAR)");
|
||||
jdbc.execute("CREATE TABLE fl_loading_pick_task(id BIGINT,loading_plan_id BIGINT,task_status VARCHAR)");
|
||||
jdbc.execute("CREATE TABLE fl_loading_pick_item(pick_task_id BIGINT,loading_plan_item_id BIGINT,item_status VARCHAR,picked_package_count INT)");
|
||||
jdbc.execute("CREATE TABLE fl_container_booking(id BIGINT,cabinet_no VARCHAR,container_no VARCHAR)");
|
||||
jdbc.execute("CREATE TABLE fl_carrier_schedule_node_fact(id BIGINT,container_booking_id BIGINT,standard_node VARCHAR,node_sequence INT,provider_code VARCHAR,estimated_at TIMESTAMP,actual_at TIMESTAMP,location_code VARCHAR,location_name VARCHAR)");
|
||||
jdbc.update("INSERT INTO fl_inbound_order VALUES(1,100,77,'NY-1',0),(2,100,77,'NY-2',0),(3,200,88,'OTHER',0),(4,100,99,'OWN-OTHER-ORDER',0)");
|
||||
jdbc.update("INSERT INTO fl_loading_plan VALUES(10,50,'COMPLETED'),(11,51,'COMPLETED'),(12,52,'LOADING')");
|
||||
jdbc.update("INSERT INTO fl_container_booking VALUES(50,'D1','TSTU0000001'),(51,'D2','TSTU0000002'),(52,'D3','TSTU0000003')");
|
||||
jdbc.update("INSERT INTO fl_loading_pick_task VALUES(20,10,'COMPLETED'),(21,11,'COMPLETED'),(22,12,'LOADING')");
|
||||
jdbc.update("INSERT INTO fl_loading_plan_item VALUES(101,10,1,100,0),(102,10,1,100,0),(103,10,2,100,0),(104,10,3,200,0),(105,10,4,100,0),(106,11,2,100,0),(107,12,2,100,0)");
|
||||
jdbc.update("INSERT INTO fl_loading_pick_item VALUES(20,101,'PARTIAL',1),(20,102,'PICKED',1),(20,103,'SKIPPED',0),(20,104,'PICKED',1),(20,105,'PICKED',1),(21,106,'PICKED',1),(22,107,'PARTIAL',1)");
|
||||
jdbc.update("INSERT INTO fl_carrier_schedule_node_fact VALUES(1,50,'AT_ORIGIN_PORT',20,'MANUAL',NULL,'2026-10-05 09:00:00','CNSZX','深圳'),(2,50,'DEPARTED_ORIGIN_PORT',40,'MATSON','2026-10-07 08:00:00',NULL,NULL,NULL),(3,51,'AT_ORIGIN_PORT',20,'MATSON',NULL,'2026-10-06 10:00:00',NULL,NULL)");
|
||||
var config = new MybatisConfiguration();
|
||||
config.setMapUnderscoreToCamelCase(true); config.addMapper(UnifiedInboundTransportMapper.class);
|
||||
var factory = new MybatisSqlSessionFactoryBean(); factory.setDataSource(ds); factory.setConfiguration(config);
|
||||
var mapper = new SqlSessionTemplate(factory.getObject()).getMapper(UnifiedInboundTransportMapper.class);
|
||||
repository = new UnifiedInboundReadRepository(); ReflectionTestUtils.setField(repository, "transports", mapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void separatesSplitFragmentsAndKeepsActualPortEntryDistinctFromEstimatedSailing() {
|
||||
var fragments = repository.findCustomerTransportFragments(100,77);
|
||||
assertThat(fragments).hasSize(2);
|
||||
assertThat(fragments.get(0).inboundCode()).isEqualTo("NY-1");
|
||||
assertThat(fragments.get(0).nodes()).hasSize(2); // two SKU memberships do not duplicate facts
|
||||
assertThat(fragments.get(0).nodes().get(0).standardNode()).isEqualTo("AT_ORIGIN_PORT");
|
||||
assertThat(fragments.get(0).nodes().get(0).actualAt()).isEqualTo(LocalDateTime.of(2026,10,5,9,0));
|
||||
assertThat(fragments.get(0).nodes().get(1).actualAt()).isNull();
|
||||
assertThat(fragments.get(0).nodes().get(1).estimatedAt()).isNotNull();
|
||||
assertThat(fragments.get(1).cabinetNo()).isEqualTo("D2");
|
||||
assertThat(fragments.get(1).nodes()).singleElement().satisfies(n ->
|
||||
assertThat(n.actualAt()).isEqualTo(LocalDateTime.of(2026,10,6,10,0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameCustomerOtherOrderAndOtherCustomerCannotReadThisOrdersFragments() {
|
||||
assertThat(repository.findCustomerTransportFragments(200,77)).isEmpty();
|
||||
assertThat(repository.findCustomerTransportFragments(100,88)).isEmpty();
|
||||
assertThat(repository.findCustomerTransportFragments(100,99)).singleElement()
|
||||
.satisfies(fragment -> assertThat(fragment.inboundCode()).isEqualTo("OWN-OTHER-ORDER"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void wholeSkippedAndDeletedMembershipsCannotGrantTransportAccess() {
|
||||
jdbc.update("UPDATE fl_loading_pick_item SET item_status='SKIPPED',picked_package_count=0 WHERE loading_plan_item_id IN(101,102,106)");
|
||||
assertThat(repository.findCustomerTransportFragments(100,77)).isEmpty();
|
||||
jdbc.update("UPDATE fl_loading_pick_item SET item_status='PICKED',picked_package_count=1 WHERE loading_plan_item_id=101");
|
||||
jdbc.update("UPDATE fl_loading_plan_item SET deleted=1 WHERE id=101");
|
||||
assertThat(repository.findCustomerTransportFragments(100,77)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneInboundInMultipleCompletedCabinetsRetainsEachFactAndEmptyNodeLists() {
|
||||
jdbc.update("UPDATE fl_loading_plan_item SET inbound_id=1 WHERE id=106");
|
||||
jdbc.update("DELETE FROM fl_carrier_schedule_node_fact WHERE container_booking_id=51");
|
||||
var fragments=repository.findCustomerTransportFragments(100,77);
|
||||
assertThat(fragments).extracting(f -> f.inboundCode()).containsExactly("NY-1","NY-1");
|
||||
assertThat(fragments.get(1).nodes()).isEmpty();
|
||||
}
|
||||
}
|
||||
+22
@@ -138,4 +138,26 @@ class UnifiedInboundQueryServiceTest {
|
||||
assertThatThrownBy(() -> new UnifiedInboundQueryService().page(query))
|
||||
.isInstanceOf(BizException.class);
|
||||
}
|
||||
@Test
|
||||
void remainingWaitSplitFragmentDoesNotHideOlderLoadedFragmentsOfSameOrder() {
|
||||
var inbounds = mock(InboundOrderRepository.class);
|
||||
var operations = mock(UnifiedInboundReadRepository.class);
|
||||
var pricing = mock(FbaPricingRepository.class);
|
||||
var service = new UnifiedInboundQueryService();
|
||||
ReflectionTestUtils.setField(service,"inbounds",inbounds);
|
||||
ReflectionTestUtils.setField(service,"operations",operations);
|
||||
ReflectionTestUtils.setField(service,"pricing",pricing);
|
||||
var remaining = new InboundOrderEntity();
|
||||
remaining.setId(88L); remaining.setOrderId(99L); remaining.setTenantId(10001L);
|
||||
remaining.setProcessStatus("WAIT_SPLIT");
|
||||
var loaded = new InboundOrderEntity(); loaded.setProcessStatus("LOADED");
|
||||
when(inbounds.findByOrderId(10001L,99L)).thenReturn(remaining);
|
||||
when(inbounds.findByOrderIds(10001L,List.of(99L))).thenReturn(List.of(remaining,loaded));
|
||||
when(inbounds.detail(10001L,remaining)).thenReturn(new InboundOrderDetail(null,null,null,null,null,List.of()));
|
||||
var fragment = new com.baiyue.firstleg.dto.CustomerTransportFragment("NY-1","D1","TSTU0000001",List.of());
|
||||
when(operations.findCustomerTransportFragments(10001L,99L)).thenReturn(List.of(fragment));
|
||||
assertThat(service.detail(10001L,99L).transportFragments()).containsExactly(fragment);
|
||||
org.mockito.Mockito.verify(operations).findCustomerTransportFragments(10001L,99L);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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