package com.baiyue.integration.provider; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import java.time.LocalDateTime; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; class ZimCarrierScheduleProviderAdapterTest { private final ObjectMapper objectMapper = new ObjectMapper(); @Test void v2FixtureSelectsOnlyTheExactContainerAndSeparatesActualFromPlans() throws Exception { String fixture = """ {"bookingReference":" BK-STRING-001 ","blReference":" BL-STRING-001 ", "finalETA":{"etaValue":"2023-09-03T08:00:00","etaValueDate":"2023-09-03","podInd":"ETA"}, "routeDetails":[ {"sailingDateTz":"2023-08-10T12:00:00","arrivalDateTz":"2023-08-17T06:00:00","portCode":"CNNGB","port":"Ningbo","country":"CN","vesselName":"V1","voyage":"001E","leg":"1"}, {"sailingDateDT":"2023-08-18T10:00:00","arrivalDateDT":"2023-09-02T04:00:00","portCode":"USNYC","port":"New York","country":"US","vesselName":"V2","voyage":"002E","leg":"2"}, {"portCode":"USNYC","port":"New York","country":"US"}], "bkBlDetails":{"consContainerList":[ {"containerNumber":"SIBLING000001 ","containerEventsList":[ {"activityDateTz":"2023-08-01T01:00:00","eventName":"Export Gate-In to Port of Loading","portCode":"CNNGB"}]}, {"containerNumber":" zcsu7881970 ","containerEventsList":[ {"activityDateTz":"2023-08-01T01:00:00","eventName":"Empty container dispatched from Port of Loading to Customer","portCode":"CNNGB"}, {"activityDateTz":"2023-08-10T02:00:00","eventName":"Export gate-in from Customer to Port of Loading","portCode":"CNNGB","port":"Ningbo","country":"CN"}, {"activityDateTz":"2023-08-18T11:00:00","eventName":"Vessel departure from first Transshipment Port to next Transshipment Port","portCode":"SGSIN","port":"Singapore","country":"SG","vesselName":"V2","voyage":"002E","leg":"2"}, {"activityDateTz":"2023-08-19T12:00:00","eventName":"Container was loaded at Transshipment Port to Transshipment Port","portCode":"SGSIN","port":"Singapore","country":"SG","vesselName":"V2","voyage":"002E","leg":"2"}, {"activityDate":"2023-08-20T12:00:00","eventName":"Container was loaded at Transshipment Port to Transshipment Port","portCode":"SGSIN","port":"Singapore","country":"SG","vesselName":"V2","voyage":"002E","leg":"2"}, {"activityDateTz":"2023-08-21T12:00:00","eventName":"Unknown Future Event","portCode":"SGSIN"}]}]}} """; var payload = objectMapper.readValue(fixture, ZimTrackingClient.TracingPayload.class); var response = new ZimTrackingClient.TrackingResponse(payload, fixture); LocalDateTime queriedAt = LocalDateTime.of(2026, 8, 21, 12, 0); var values = new ZimCarrierScheduleProviderAdapter(mock(ZimTrackingClient.class)) .observations("ZCSU7881970", response, queriedAt); assertThat(values).hasSize(7); assertThat(values).extracting(value -> value.observedAt()).containsOnly(queriedAt); assertThat(values).filteredOn(value -> value.actualAt() != null) .extracting(value -> value.standardNode()) .containsExactlyInAnyOrder("AT_ORIGIN_PORT", "IN_TRANSIT", "LOADED_ON_VESSEL", "LOADED_ON_VESSEL"); assertThat(values).filteredOn(value -> value.estimatedAt() != null) .extracting(value -> value.standardNode()) .containsExactlyInAnyOrder("DEPARTED_ORIGIN_PORT", "ARRIVED_DESTINATION_PORT", "ARRIVED_DESTINATION_PORT"); assertThat(values).noneMatch(value -> LocalDateTime.of(2023, 8, 1, 1, 0).equals(value.actualAt())); assertThat(values).extracting(value -> value.providerNodeCode()) .doesNotContain("EMPTY CONTAINER DISPATCHED FROM PORT OF LOADING TO CUSTOMER", "UNKNOWN FUTURE EVENT"); assertThat(values).allSatisfy(value -> assertThat(value.sourceReference()) .isEqualTo("ZIM:CONTAINER:ZCSU7881970|BOOKING:BK-STRING-001|BL:BL-STRING-001")); } @Test void eventNameMappingIsAnExactExpandableWhitelist() { assertThat(ZimCarrierScheduleProviderAdapter.standardNode( "Export Gate-In to Port of Loading")).isEqualTo("AT_ORIGIN_PORT"); assertThat(ZimCarrierScheduleProviderAdapter.standardNode( "Container was loaded at Transshipment Port to Transshipment Port")).isEqualTo("LOADED_ON_VESSEL"); assertThat(ZimCarrierScheduleProviderAdapter.standardNode( "Vessel departure from first Transshipment Port to next Transshipment Port")).isEqualTo("IN_TRANSIT"); assertThat(ZimCarrierScheduleProviderAdapter.standardNode( "Container loaded at Port of Loading")).isEqualTo("LOADED_ON_VESSEL"); assertThat(ZimCarrierScheduleProviderAdapter.standardNode( "Vessel departure from Port of Loading")).isEqualTo("DEPARTED_ORIGIN_PORT"); assertThat(ZimCarrierScheduleProviderAdapter.standardNode( "Container was loaded at Port of Loading to Port of Discharge")).isEqualTo("LOADED_ON_VESSEL"); assertThat(ZimCarrierScheduleProviderAdapter.standardNode( "Vessel departure from Port of Loading to Port of Discharge")).isEqualTo("DEPARTED_ORIGIN_PORT"); assertThat(ZimCarrierScheduleProviderAdapter.standardNode( "Empty container dispatched from Port of Loading to Customer")).isNull(); assertThat(ZimCarrierScheduleProviderAdapter.standardNode("Unknown Future Event")).isNull(); } @Test void liveBkBlOnlyFixtureToleratesMissingPlansAndDoesNotAdvanceCarrierRelease() throws Exception { String fixture = """ {"bkBlDetails":{"consContainerList":[ {"containerNumber":" ZCSU7205614 ","containerEventsList":[ {"activityDateTz":"2026-08-01T01:00:00Z","eventName":"Empty container dispatched from inland point to Customer","portCode":"CNNGB"}, {"activityDateTz":"2026-08-03T02:00:00Z","eventName":"Export gate-in from Customer to Port of Loading","portCode":"CNNGB","port":"Ningbo","country":"CN"}, {"activityDateTz":"2026-08-05T03:00:00Z","eventName":"Container was loaded at Port of Loading to Port of Discharge","portCode":"CNNGB","port":"Ningbo","country":"CN","vesselName":"LIVE VESSEL","voyage":"001E","leg":"1"}, {"activityDateTz":"2026-08-06T04:00:00Z","eventName":"Vessel departure from Port of Loading to Port of Discharge","portCode":"CNNGB","port":"Ningbo","country":"CN","vesselName":"LIVE VESSEL","voyage":"001E","leg":"1"}, {"activityDateTz":"2026-08-20T05:00:00Z","eventName":"Carrier Release","portCode":"USLAX","port":"Los Angeles","country":"US"}]}, {"containerNumber":"ZCSU7960370","containerEventsList":[ {"activityDateTz":"2026-08-07T00:00:00Z","eventName":"Export gate-in from Customer to Port of Loading"}]}]}} """; var payload = objectMapper.readValue(fixture, ZimTrackingClient.TracingPayload.class); var response = new ZimTrackingClient.TrackingResponse(payload, fixture); var values = new ZimCarrierScheduleProviderAdapter(mock(ZimTrackingClient.class)) .observations("zcsu7205614", response, LocalDateTime.of(2026, 8, 21, 12, 0)); assertThat(payload.routeDetails()).isNull(); assertThat(payload.finalETA()).isNull(); assertThat(values).hasSize(3).allSatisfy(value -> assertThat(value.actualAt()).isNotNull()); assertThat(values).extracting(value -> value.standardNode()) .containsExactlyInAnyOrder("AT_ORIGIN_PORT", "LOADED_ON_VESSEL", "DEPARTED_ORIGIN_PORT"); assertThat(values).extracting(value -> value.standardNode()).doesNotContain("PICKUP_READY"); assertThat(values).extracting(value -> value.providerNodeCode()) .doesNotContain("CARRIER RELEASE", "EMPTY CONTAINER DISPATCHED FROM INLAND POINT TO CUSTOMER"); } }