feat(orders): expose customer-owned loaded transport facts

This commit is contained in:
2026-10-05 01:06:11 +08:00
parent 5ad3fec449
commit b000b056d1
15 changed files with 286 additions and 9 deletions
@@ -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");
}
}
@@ -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();
}
}
@@ -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);
}
}