feat(firstleg): expose batch carrier actual facts

This commit is contained in:
2026-09-08 19:09:08 +08:00
parent 75e67fa980
commit b0c81e337d
10 changed files with 182 additions and 0 deletions
@@ -14,6 +14,18 @@ class CarrierScheduleQueryOpenApiContractTest {
.getContentAsString(StandardCharsets.UTF_8);
assertThat(yaml)
.contains("/api/platform/v1/carrier-schedule-queries:")
.contains("/api/platform/v1/carrier-schedule-queries/container-actual-facts:")
.contains("name: containerBookingIds")
.contains("maxItems: 200")
.contains("uniqueItems: true")
.contains("ContainerActualFacts:")
.contains("departedWarehouseAt:")
.contains("departedOriginPortAt:")
.contains("arrivedDestinationPortAt:")
.contains("pickupReadyAt:")
.contains("pickedUpAt:")
.contains("emptyReturnedAt:")
.contains("Estimated facts are never returned or inferred")
.contains("/api/platform/v1/carrier-schedule-queries/{queryId}/manual-results:")
.contains("/api/platform/v1/carrier-schedule-queries/{queryId}/manual-nodes:")
.contains("x-permission: platform:firstleg:port-schedule-query:manual")
@@ -18,6 +18,7 @@ class PlatformCarrierScheduleQueryControllerContractTest {
.containsExactly("/api/platform/v1/carrier-schedule-queries");
Map<String, String> permissions = Map.of(
"carriers", "platform:firstleg:port-schedule-query:list",
"containerActualFacts", "platform:firstleg:port-schedule-query:list",
"page", "platform:firstleg:port-schedule-query:list",
"detail", "platform:firstleg:port-schedule-query:list",
"history", "platform:firstleg:port-schedule-query:list",
@@ -9,10 +9,31 @@ import org.springframework.core.io.ClassPathResource;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class CarrierScheduleQuerySqlContractTest {
@Test
void batchesActualContainerFactsWithoutEstimatedOrPerNodeQueries() throws Exception {
String sql = String.join("\n", CarrierScheduleQueryMapper.class
.getMethod("selectContainerActualFacts", List.class)
.getAnnotation(Select.class).value());
assertThat(sql)
.contains("container_booking_id IN")
.contains("f.actual_at IS NOT NULL")
.contains("MAX(CASE WHEN f.standard_node='DEPARTED_WAREHOUSE'")
.contains("MAX(CASE WHEN f.standard_node='DEPARTED_ORIGIN_PORT'")
.contains("MAX(CASE WHEN f.standard_node='ARRIVED_DESTINATION_PORT'")
.contains("MAX(CASE WHEN f.standard_node='PICKUP_READY'")
.contains("MAX(CASE WHEN f.standard_node='PICKED_UP'")
.contains("MAX(CASE WHEN f.standard_node='EMPTY_RETURNED'")
.contains("ORDER BY q.container_booking_id")
.doesNotContain("estimated_at");
assertThat(count(sql, "fl_carrier_schedule_node_fact")).isEqualTo(1);
}
@Test
void annotationSqlUsesExecutableComparisonOperators() throws Exception {
String selectDue = String.join("\n", CarrierScheduleQueryMapper.class
@@ -149,4 +170,8 @@ class CarrierScheduleQuerySqlContractTest {
return new ClassPathResource("db/phase3-carrier-schedule-query-schema.sql")
.getContentAsString(StandardCharsets.UTF_8);
}
private static int count(String source, String needle) {
return source.split(java.util.regex.Pattern.quote(needle), -1).length - 1;
}
}
@@ -4,6 +4,7 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CreateRequest;
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerActualFacts;
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualResultRequest;
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualNodeRequest;
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ProviderObservationRequest;
@@ -72,6 +73,35 @@ class CarrierScheduleQueryServiceTest {
when(portSchedules.containerBooking(20L)).thenReturn(container);
}
@Test
void returnsBatchActualFactsFromOneRepositoryCall() {
ContainerActualFacts facts = new ContainerActualFacts(20L, 30L, 2, "READY",
"PICKUP_READY", NOW, NOW.minusDays(12), NOW.minusDays(10), NOW.minusDays(2),
NOW, null, null);
when(repository.containerActualFacts(List.of(20L, 21L))).thenReturn(List.of(facts));
assertThat(service.containerActualFacts(List.of(20L, 21L))).containsExactly(facts);
verify(repository).containerActualFacts(List.of(20L, 21L));
}
@Test
void rejectsInvalidActualFactBatchBeforeRepositoryAccess() {
assertThatThrownBy(() -> service.containerActualFacts(null))
.isInstanceOf(BizException.class).hasMessageContaining("1到200");
assertThatThrownBy(() -> service.containerActualFacts(List.of()))
.isInstanceOf(BizException.class).hasMessageContaining("1到200");
assertThatThrownBy(() -> service.containerActualFacts(java.util.Collections.nCopies(201, 1L)))
.isInstanceOf(BizException.class).hasMessageContaining("1到200");
assertThatThrownBy(() -> service.containerActualFacts(java.util.Arrays.asList(1L, null)))
.isInstanceOf(BizException.class).hasMessageContaining("正数");
assertThatThrownBy(() -> service.containerActualFacts(List.of(1L, 0L)))
.isInstanceOf(BizException.class).hasMessageContaining("正数");
assertThatThrownBy(() -> service.containerActualFacts(List.of(1L, 1L)))
.isInstanceOf(BizException.class).hasMessageContaining("重复");
verify(repository, never()).containerActualFacts(any());
}
@Test
void createsTaskOnlyForBookingCustomerAndAllocatedContainer() {
OceanBookingEntity booking = new OceanBookingEntity(); booking.setCustomerId(10001L);