@freighttech/shipment-tracking 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +25 -0
- package/dist/generated/entities/bic_config/index.js +21 -0
- package/dist/generated/entities/bic_config/index.js.map +7 -0
- package/dist/generated/entities/carrier_config/index.js +27 -0
- package/dist/generated/entities/carrier_config/index.js.map +7 -0
- package/dist/generated/entities/location_override/index.js +33 -0
- package/dist/generated/entities/location_override/index.js.map +7 -0
- package/dist/generated/entities/shipment/index.js +73 -0
- package/dist/generated/entities/shipment/index.js.map +7 -0
- package/dist/generated/entities/ships_go_config/index.js +31 -0
- package/dist/generated/entities/ships_go_config/index.js.map +7 -0
- package/dist/generated/entities/tracking_event/index.js +101 -0
- package/dist/generated/entities/tracking_event/index.js.map +7 -0
- package/dist/generated/entities/tracking_job/index.js +47 -0
- package/dist/generated/entities/tracking_job/index.js.map +7 -0
- package/dist/generated/entities/webhook/index.js +25 -0
- package/dist/generated/entities/webhook/index.js.map +7 -0
- package/dist/generated/entities/webhook_delivery/index.js +25 -0
- package/dist/generated/entities/webhook_delivery/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +21 -0
- package/dist/generated/entities.ids.generated.js.map +7 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/shipment_tracking/acl.js +22 -0
- package/dist/modules/shipment_tracking/acl.js.map +7 -0
- package/dist/modules/shipment_tracking/api/bic-configs/route.js +176 -0
- package/dist/modules/shipment_tracking/api/bic-configs/route.js.map +7 -0
- package/dist/modules/shipment_tracking/api/bic-configs/test/route.js +95 -0
- package/dist/modules/shipment_tracking/api/bic-configs/test/route.js.map +7 -0
- package/dist/modules/shipment_tracking/api/cargo-events/route.js +93 -0
- package/dist/modules/shipment_tracking/api/cargo-events/route.js.map +7 -0
- package/dist/modules/shipment_tracking/api/carrier-configs/route.js +139 -0
- package/dist/modules/shipment_tracking/api/carrier-configs/route.js.map +7 -0
- package/dist/modules/shipment_tracking/api/dashboard/ships-map/route.js +177 -0
- package/dist/modules/shipment_tracking/api/dashboard/ships-map/route.js.map +7 -0
- package/dist/modules/shipment_tracking/api/location-overrides/route.js +179 -0
- package/dist/modules/shipment_tracking/api/location-overrides/route.js.map +7 -0
- package/dist/modules/shipment_tracking/api/openapi.js +27 -0
- package/dist/modules/shipment_tracking/api/openapi.js.map +7 -0
- package/dist/modules/shipment_tracking/api/shipments/route.js +192 -0
- package/dist/modules/shipment_tracking/api/shipments/route.js.map +7 -0
- package/dist/modules/shipment_tracking/api/shipsgo-configs/route.js +185 -0
- package/dist/modules/shipment_tracking/api/shipsgo-configs/route.js.map +7 -0
- package/dist/modules/shipment_tracking/api/tracking-jobs/route.js +144 -0
- package/dist/modules/shipment_tracking/api/tracking-jobs/route.js.map +7 -0
- package/dist/modules/shipment_tracking/api/tracking-jobs/sync/route.js +107 -0
- package/dist/modules/shipment_tracking/api/tracking-jobs/sync/route.js.map +7 -0
- package/dist/modules/shipment_tracking/api/utils.js +12 -0
- package/dist/modules/shipment_tracking/api/utils.js.map +7 -0
- package/dist/modules/shipment_tracking/api/webhooks/deliveries/route.js +76 -0
- package/dist/modules/shipment_tracking/api/webhooks/deliveries/route.js.map +7 -0
- package/dist/modules/shipment_tracking/api/webhooks/route.js +135 -0
- package/dist/modules/shipment_tracking/api/webhooks/route.js.map +7 -0
- package/dist/modules/shipment_tracking/api/webhooks/test/route.js +71 -0
- package/dist/modules/shipment_tracking/api/webhooks/test/route.js.map +7 -0
- package/dist/modules/shipment_tracking/assets/carriers/cma_cgm.png +0 -0
- package/dist/modules/shipment_tracking/assets/carriers/cosco.png +0 -0
- package/dist/modules/shipment_tracking/assets/carriers/evergreen.png +0 -0
- package/dist/modules/shipment_tracking/assets/carriers/hapag_lloyd.png +0 -0
- package/dist/modules/shipment_tracking/assets/carriers/maersk.png +0 -0
- package/dist/modules/shipment_tracking/assets/carriers/msc.png +0 -0
- package/dist/modules/shipment_tracking/assets/carriers/one.png +0 -0
- package/dist/modules/shipment_tracking/assets/carriers/oocl.png +0 -0
- package/dist/modules/shipment_tracking/assets/carriers/yang_ming.png +0 -0
- package/dist/modules/shipment_tracking/assets/carriers/zim.png +0 -0
- package/dist/modules/shipment_tracking/assets/index.js +52 -0
- package/dist/modules/shipment_tracking/assets/index.js.map +7 -0
- package/dist/modules/shipment_tracking/backend/carrier-configs/page.js +621 -0
- package/dist/modules/shipment_tracking/backend/carrier-configs/page.js.map +7 -0
- package/dist/modules/shipment_tracking/backend/carrier-configs/page.meta.js +19 -0
- package/dist/modules/shipment_tracking/backend/carrier-configs/page.meta.js.map +7 -0
- package/dist/modules/shipment_tracking/backend/location-overrides/page.js +198 -0
- package/dist/modules/shipment_tracking/backend/location-overrides/page.js.map +7 -0
- package/dist/modules/shipment_tracking/backend/location-overrides/page.meta.js +19 -0
- package/dist/modules/shipment_tracking/backend/location-overrides/page.meta.js.map +7 -0
- package/dist/modules/shipment_tracking/backend/shipment-tracking/page.js +279 -0
- package/dist/modules/shipment_tracking/backend/shipment-tracking/page.js.map +7 -0
- package/dist/modules/shipment_tracking/backend/shipment-tracking/page.meta.js +19 -0
- package/dist/modules/shipment_tracking/backend/shipment-tracking/page.meta.js.map +7 -0
- package/dist/modules/shipment_tracking/backend/tracking-jobs/page.js +10 -0
- package/dist/modules/shipment_tracking/backend/tracking-jobs/page.js.map +7 -0
- package/dist/modules/shipment_tracking/backend/tracking-jobs/page.meta.js +19 -0
- package/dist/modules/shipment_tracking/backend/tracking-jobs/page.meta.js.map +7 -0
- package/dist/modules/shipment_tracking/backend/webhooks/page.js +198 -0
- package/dist/modules/shipment_tracking/backend/webhooks/page.js.map +7 -0
- package/dist/modules/shipment_tracking/backend/webhooks/page.meta.js +19 -0
- package/dist/modules/shipment_tracking/backend/webhooks/page.meta.js.map +7 -0
- package/dist/modules/shipment_tracking/ce.js +52 -0
- package/dist/modules/shipment_tracking/ce.js.map +7 -0
- package/dist/modules/shipment_tracking/cli.js +229 -0
- package/dist/modules/shipment_tracking/cli.js.map +7 -0
- package/dist/modules/shipment_tracking/commands/bic-configs.js +66 -0
- package/dist/modules/shipment_tracking/commands/bic-configs.js.map +7 -0
- package/dist/modules/shipment_tracking/commands/carrier-configs.js +65 -0
- package/dist/modules/shipment_tracking/commands/carrier-configs.js.map +7 -0
- package/dist/modules/shipment_tracking/commands/index.js +9 -0
- package/dist/modules/shipment_tracking/commands/index.js.map +7 -0
- package/dist/modules/shipment_tracking/commands/location-overrides.js +93 -0
- package/dist/modules/shipment_tracking/commands/location-overrides.js.map +7 -0
- package/dist/modules/shipment_tracking/commands/shipments.js +200 -0
- package/dist/modules/shipment_tracking/commands/shipments.js.map +7 -0
- package/dist/modules/shipment_tracking/commands/shipsgo-configs.js +75 -0
- package/dist/modules/shipment_tracking/commands/shipsgo-configs.js.map +7 -0
- package/dist/modules/shipment_tracking/commands/tracking-jobs.js +120 -0
- package/dist/modules/shipment_tracking/commands/tracking-jobs.js.map +7 -0
- package/dist/modules/shipment_tracking/commands/tracking.js +140 -0
- package/dist/modules/shipment_tracking/commands/tracking.js.map +7 -0
- package/dist/modules/shipment_tracking/commands/webhooks.js +115 -0
- package/dist/modules/shipment_tracking/commands/webhooks.js.map +7 -0
- package/dist/modules/shipment_tracking/components/CombinedTimestampCell.js +168 -0
- package/dist/modules/shipment_tracking/components/CombinedTimestampCell.js.map +7 -0
- package/dist/modules/shipment_tracking/components/FacilityLocationInput.js +202 -0
- package/dist/modules/shipment_tracking/components/FacilityLocationInput.js.map +7 -0
- package/dist/modules/shipment_tracking/components/LocationOverrideDrawer.js +501 -0
- package/dist/modules/shipment_tracking/components/LocationOverrideDrawer.js.map +7 -0
- package/dist/modules/shipment_tracking/components/ShipmentDetailsDrawer.js +818 -0
- package/dist/modules/shipment_tracking/components/ShipmentDetailsDrawer.js.map +7 -0
- package/dist/modules/shipment_tracking/components/ShipmentDrawer.js +404 -0
- package/dist/modules/shipment_tracking/components/ShipmentDrawer.js.map +7 -0
- package/dist/modules/shipment_tracking/components/ShipsGoConfigSection.js +200 -0
- package/dist/modules/shipment_tracking/components/ShipsGoConfigSection.js.map +7 -0
- package/dist/modules/shipment_tracking/components/TimestampCell.js +108 -0
- package/dist/modules/shipment_tracking/components/TimestampCell.js.map +7 -0
- package/dist/modules/shipment_tracking/components/TrackingJobDrawer.js +343 -0
- package/dist/modules/shipment_tracking/components/TrackingJobDrawer.js.map +7 -0
- package/dist/modules/shipment_tracking/components/VesselTrackingMap.js +496 -0
- package/dist/modules/shipment_tracking/components/VesselTrackingMap.js.map +7 -0
- package/dist/modules/shipment_tracking/components/WebhookDrawer.js +332 -0
- package/dist/modules/shipment_tracking/components/WebhookDrawer.js.map +7 -0
- package/dist/modules/shipment_tracking/data/entities.js +686 -0
- package/dist/modules/shipment_tracking/data/entities.js.map +7 -0
- package/dist/modules/shipment_tracking/data/validators.js +434 -0
- package/dist/modules/shipment_tracking/data/validators.js.map +7 -0
- package/dist/modules/shipment_tracking/di.js +60 -0
- package/dist/modules/shipment_tracking/di.js.map +7 -0
- package/dist/modules/shipment_tracking/encryption.js +12 -0
- package/dist/modules/shipment_tracking/encryption.js.map +7 -0
- package/dist/modules/shipment_tracking/events.js +87 -0
- package/dist/modules/shipment_tracking/events.js.map +7 -0
- package/dist/modules/shipment_tracking/hooks/useVesselTracking.js +137 -0
- package/dist/modules/shipment_tracking/hooks/useVesselTracking.js.map +7 -0
- package/dist/modules/shipment_tracking/i18n/de.json +327 -0
- package/dist/modules/shipment_tracking/i18n/en.json +327 -0
- package/dist/modules/shipment_tracking/i18n/es.json +327 -0
- package/dist/modules/shipment_tracking/i18n/pl.json +327 -0
- package/dist/modules/shipment_tracking/index.js +15 -0
- package/dist/modules/shipment_tracking/index.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/adapters/cma-cgm.js +66 -0
- package/dist/modules/shipment_tracking/lib/adapters/cma-cgm.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/adapters/cosco-types.js +1 -0
- package/dist/modules/shipment_tracking/lib/adapters/cosco-types.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/adapters/cosco.js +236 -0
- package/dist/modules/shipment_tracking/lib/adapters/cosco.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/adapters/evergreen.js +87 -0
- package/dist/modules/shipment_tracking/lib/adapters/evergreen.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/adapters/hapag-lloyd.js +68 -0
- package/dist/modules/shipment_tracking/lib/adapters/hapag-lloyd.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/adapters/index.js +20 -0
- package/dist/modules/shipment_tracking/lib/adapters/index.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/adapters/maersk.js +84 -0
- package/dist/modules/shipment_tracking/lib/adapters/maersk.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/adapters/msc.js +170 -0
- package/dist/modules/shipment_tracking/lib/adapters/msc.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/adapters/zim.js +85 -0
- package/dist/modules/shipment_tracking/lib/adapters/zim.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/audit-helpers.js +20 -0
- package/dist/modules/shipment_tracking/lib/audit-helpers.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/auth/base64url.js +8 -0
- package/dist/modules/shipment_tracking/lib/auth/base64url.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/auth/oauth-client.js +38 -0
- package/dist/modules/shipment_tracking/lib/auth/oauth-client.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/background.js +15 -0
- package/dist/modules/shipment_tracking/lib/background.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/bic-api-client.js +106 -0
- package/dist/modules/shipment_tracking/lib/bic-api-client.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/carrier-adapter.js +17 -0
- package/dist/modules/shipment_tracking/lib/carrier-adapter.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/current-vessel.js +149 -0
- package/dist/modules/shipment_tracking/lib/current-vessel.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/dcsa-event-mapping.js +124 -0
- package/dist/modules/shipment_tracking/lib/dcsa-event-mapping.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/dcsa-event-parser.js +112 -0
- package/dist/modules/shipment_tracking/lib/dcsa-event-parser.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/dcsa-params.js +16 -0
- package/dist/modules/shipment_tracking/lib/dcsa-params.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/empty-return.js +48 -0
- package/dist/modules/shipment_tracking/lib/empty-return.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/location-overrides.js +85 -0
- package/dist/modules/shipment_tracking/lib/location-overrides.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/location-types.js +59 -0
- package/dist/modules/shipment_tracking/lib/location-types.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/logger.js +113 -0
- package/dist/modules/shipment_tracking/lib/logger.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/poi-event-processor.js +183 -0
- package/dist/modules/shipment_tracking/lib/poi-event-processor.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/poi-nats-config.js +52 -0
- package/dist/modules/shipment_tracking/lib/poi-nats-config.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/poi-types.js +43 -0
- package/dist/modules/shipment_tracking/lib/poi-types.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/rate-limiter.js +31 -0
- package/dist/modules/shipment_tracking/lib/rate-limiter.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/route-extraction.js +187 -0
- package/dist/modules/shipment_tracking/lib/route-extraction.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/route-inference.js +106 -0
- package/dist/modules/shipment_tracking/lib/route-inference.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/schedule-generator.js +63 -0
- package/dist/modules/shipment_tracking/lib/schedule-generator.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/shipsgo-client.js +143 -0
- package/dist/modules/shipment_tracking/lib/shipsgo-client.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/shipsgo-mapper.js +204 -0
- package/dist/modules/shipment_tracking/lib/shipsgo-mapper.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/status-machine.js +131 -0
- package/dist/modules/shipment_tracking/lib/status-machine.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/time-extraction.js +41 -0
- package/dist/modules/shipment_tracking/lib/time-extraction.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/timestamp-utils.js +100 -0
- package/dist/modules/shipment_tracking/lib/timestamp-utils.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/vessel-api.js +111 -0
- package/dist/modules/shipment_tracking/lib/vessel-api.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/vessel-cache.js +50 -0
- package/dist/modules/shipment_tracking/lib/vessel-cache.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/vessel-extraction.js +13 -0
- package/dist/modules/shipment_tracking/lib/vessel-extraction.js.map +7 -0
- package/dist/modules/shipment_tracking/lib/webhook-dispatcher.js +62 -0
- package/dist/modules/shipment_tracking/lib/webhook-dispatcher.js.map +7 -0
- package/dist/modules/shipment_tracking/migrations/Migration20260422161208_shipment_tracking.js +48 -0
- package/dist/modules/shipment_tracking/migrations/Migration20260422161208_shipment_tracking.js.map +7 -0
- package/dist/modules/shipment_tracking/migrations/Migration20260529120000_shipment_tracking_poi_region_names.js +15 -0
- package/dist/modules/shipment_tracking/migrations/Migration20260529120000_shipment_tracking_poi_region_names.js.map +7 -0
- package/dist/modules/shipment_tracking/migrations/Migration20260821120000_shipment_tracking_shipsgo.js +47 -0
- package/dist/modules/shipment_tracking/migrations/Migration20260821120000_shipment_tracking_shipsgo.js.map +7 -0
- package/dist/modules/shipment_tracking/notifications.js +145 -0
- package/dist/modules/shipment_tracking/notifications.js.map +7 -0
- package/dist/modules/shipment_tracking/services/carrierRegistry.js +24 -0
- package/dist/modules/shipment_tracking/services/carrierRegistry.js.map +7 -0
- package/dist/modules/shipment_tracking/services/poiNatsService.js +397 -0
- package/dist/modules/shipment_tracking/services/poiNatsService.js.map +7 -0
- package/dist/modules/shipment_tracking/services/shipsGoProvider.js +163 -0
- package/dist/modules/shipment_tracking/services/shipsGoProvider.js.map +7 -0
- package/dist/modules/shipment_tracking/services/trackingService.js +1283 -0
- package/dist/modules/shipment_tracking/services/trackingService.js.map +7 -0
- package/dist/modules/shipment_tracking/services/webhookService.js +263 -0
- package/dist/modules/shipment_tracking/services/webhookService.js.map +7 -0
- package/dist/modules/shipment_tracking/setup.js +72 -0
- package/dist/modules/shipment_tracking/setup.js.map +7 -0
- package/dist/modules/shipment_tracking/subscribers/cargo-event-created.js +60 -0
- package/dist/modules/shipment_tracking/subscribers/cargo-event-created.js.map +7 -0
- package/dist/modules/shipment_tracking/subscribers/dcsa-event-webhook-dispatch.js +58 -0
- package/dist/modules/shipment_tracking/subscribers/dcsa-event-webhook-dispatch.js.map +7 -0
- package/dist/modules/shipment_tracking/subscribers/poi-event-notification.js +82 -0
- package/dist/modules/shipment_tracking/subscribers/poi-event-notification.js.map +7 -0
- package/dist/modules/shipment_tracking/subscribers/poi-event-webhook-dispatch.js +53 -0
- package/dist/modules/shipment_tracking/subscribers/poi-event-webhook-dispatch.js.map +7 -0
- package/dist/modules/shipment_tracking/subscribers/shipment-status-changed.js +28 -0
- package/dist/modules/shipment_tracking/subscribers/shipment-status-changed.js.map +7 -0
- package/dist/modules/shipment_tracking/subscribers/terminal-event-ingest.js +67 -0
- package/dist/modules/shipment_tracking/subscribers/terminal-event-ingest.js.map +7 -0
- package/dist/modules/shipment_tracking/subscribers/tracking-job-initial-poll.js +30 -0
- package/dist/modules/shipment_tracking/subscribers/tracking-job-initial-poll.js.map +7 -0
- package/dist/modules/shipment_tracking/subscribers/tracking-job-webhook-dispatch.js +19 -0
- package/dist/modules/shipment_tracking/subscribers/tracking-job-webhook-dispatch.js.map +7 -0
- package/dist/modules/shipment_tracking/tables/index.js +6 -0
- package/dist/modules/shipment_tracking/tables/index.js.map +7 -0
- package/dist/modules/shipment_tracking/tables/tracking_job/table.client.js +284 -0
- package/dist/modules/shipment_tracking/tables/tracking_job/table.client.js.map +7 -0
- package/dist/modules/shipment_tracking/tables/tracking_job/table.js +36 -0
- package/dist/modules/shipment_tracking/tables/tracking_job/table.js.map +7 -0
- package/dist/modules/shipment_tracking/widgets/dashboard/ships-map/config.js +25 -0
- package/dist/modules/shipment_tracking/widgets/dashboard/ships-map/config.js.map +7 -0
- package/dist/modules/shipment_tracking/widgets/dashboard/ships-map/widget.client.js +355 -0
- package/dist/modules/shipment_tracking/widgets/dashboard/ships-map/widget.client.js.map +7 -0
- package/dist/modules/shipment_tracking/widgets/dashboard/ships-map/widget.js +26 -0
- package/dist/modules/shipment_tracking/widgets/dashboard/ships-map/widget.js.map +7 -0
- package/dist/types/assets.d.js +1 -0
- package/dist/types/assets.d.js.map +7 -0
- package/generated/entities/bic_config/index.ts +20 -0
- package/generated/entities/carrier_config/index.ts +23 -0
- package/generated/entities/location_override/index.ts +26 -0
- package/generated/entities/shipment/index.ts +46 -0
- package/generated/entities/ships_go_config/index.ts +25 -0
- package/generated/entities/tracking_event/index.ts +60 -0
- package/generated/entities/tracking_job/index.ts +33 -0
- package/generated/entities/webhook/index.ts +22 -0
- package/generated/entities/webhook_delivery/index.ts +22 -0
- package/generated/entities.ids.generated.ts +47 -0
- package/package.json +81 -0
- package/src/index.ts +39 -0
- package/src/modules/shipment_tracking/__integration__/TC-TRACK-410-shipsgo-config-api.spec.ts +133 -0
- package/src/modules/shipment_tracking/__integration__/meta.ts +4 -0
- package/src/modules/shipment_tracking/__tests__/empty-return-stop.test.ts +231 -0
- package/src/modules/shipment_tracking/__tests__/fixtures/hapag-lloyd-transship-container.json +702 -0
- package/src/modules/shipment_tracking/__tests__/fixtures/index.ts +434 -0
- package/src/modules/shipment_tracking/__tests__/fixtures/maersk-multi-transship-completed.json +1518 -0
- package/src/modules/shipment_tracking/__tests__/fixtures/maersk-transship-intransit.json +558 -0
- package/src/modules/shipment_tracking/__tests__/fixtures/msc-direct.json +222 -0
- package/src/modules/shipment_tracking/__tests__/fixtures/msc-multi-transship-completed.json +974 -0
- package/src/modules/shipment_tracking/__tests__/fixtures/msc-multi.json +4846 -0
- package/src/modules/shipment_tracking/__tests__/fixtures/msc-transship.json +409 -0
- package/src/modules/shipment_tracking/__tests__/fixtures/poi-events.json +240 -0
- package/src/modules/shipment_tracking/__tests__/fixtures/synthetic/index.ts +138 -0
- package/src/modules/shipment_tracking/__tests__/fixtures/synthetic-eta-updates.json +288 -0
- package/src/modules/shipment_tracking/__tests__/fixtures/synthetic-route-inference.json +201 -0
- package/src/modules/shipment_tracking/__tests__/fixtures/synthetic-voyage-progression.json +410 -0
- package/src/modules/shipment_tracking/__tests__/shipsgo-air-mapper.test.ts +157 -0
- package/src/modules/shipment_tracking/__tests__/shipsgo-client.test.ts +155 -0
- package/src/modules/shipment_tracking/__tests__/shipsgo-mapper.test.ts +201 -0
- package/src/modules/shipment_tracking/__tests__/shipsgo-poll.test.ts +357 -0
- package/src/modules/shipment_tracking/__tests__/shipsgo-provider.test.ts +364 -0
- package/src/modules/shipment_tracking/__tests__/trackingService.integration.test.ts +2248 -0
- package/src/modules/shipment_tracking/__tests__/webhookPayload.test.ts +151 -0
- package/src/modules/shipment_tracking/acl.ts +18 -0
- package/src/modules/shipment_tracking/api/bic-configs/route.ts +206 -0
- package/src/modules/shipment_tracking/api/bic-configs/test/route.ts +108 -0
- package/src/modules/shipment_tracking/api/cargo-events/route.ts +101 -0
- package/src/modules/shipment_tracking/api/carrier-configs/route.ts +142 -0
- package/src/modules/shipment_tracking/api/dashboard/ships-map/route.ts +246 -0
- package/src/modules/shipment_tracking/api/location-overrides/route.ts +185 -0
- package/src/modules/shipment_tracking/api/openapi.ts +28 -0
- package/src/modules/shipment_tracking/api/shipments/route.ts +197 -0
- package/src/modules/shipment_tracking/api/shipsgo-configs/route.ts +215 -0
- package/src/modules/shipment_tracking/api/tracking-jobs/route.ts +149 -0
- package/src/modules/shipment_tracking/api/tracking-jobs/sync/route.ts +116 -0
- package/src/modules/shipment_tracking/api/utils.ts +10 -0
- package/src/modules/shipment_tracking/api/webhooks/deliveries/route.ts +82 -0
- package/src/modules/shipment_tracking/api/webhooks/route.ts +138 -0
- package/src/modules/shipment_tracking/api/webhooks/test/route.ts +84 -0
- package/src/modules/shipment_tracking/assets/carriers/cma_cgm.png +0 -0
- package/src/modules/shipment_tracking/assets/carriers/cosco.png +0 -0
- package/src/modules/shipment_tracking/assets/carriers/evergreen.png +0 -0
- package/src/modules/shipment_tracking/assets/carriers/hapag_lloyd.png +0 -0
- package/src/modules/shipment_tracking/assets/carriers/maersk.png +0 -0
- package/src/modules/shipment_tracking/assets/carriers/msc.png +0 -0
- package/src/modules/shipment_tracking/assets/carriers/one.png +0 -0
- package/src/modules/shipment_tracking/assets/carriers/oocl.png +0 -0
- package/src/modules/shipment_tracking/assets/carriers/yang_ming.png +0 -0
- package/src/modules/shipment_tracking/assets/carriers/zim.png +0 -0
- package/src/modules/shipment_tracking/assets/index.ts +66 -0
- package/src/modules/shipment_tracking/backend/carrier-configs/page.meta.ts +16 -0
- package/src/modules/shipment_tracking/backend/carrier-configs/page.tsx +782 -0
- package/src/modules/shipment_tracking/backend/location-overrides/page.meta.ts +16 -0
- package/src/modules/shipment_tracking/backend/location-overrides/page.tsx +241 -0
- package/src/modules/shipment_tracking/backend/shipment-tracking/page.meta.ts +16 -0
- package/src/modules/shipment_tracking/backend/shipment-tracking/page.tsx +340 -0
- package/src/modules/shipment_tracking/backend/tracking-jobs/page.meta.ts +16 -0
- package/src/modules/shipment_tracking/backend/tracking-jobs/page.tsx +16 -0
- package/src/modules/shipment_tracking/backend/webhooks/page.meta.ts +16 -0
- package/src/modules/shipment_tracking/backend/webhooks/page.tsx +228 -0
- package/src/modules/shipment_tracking/ce.ts +48 -0
- package/src/modules/shipment_tracking/cli.ts +321 -0
- package/src/modules/shipment_tracking/commands/bic-configs.ts +94 -0
- package/src/modules/shipment_tracking/commands/carrier-configs.ts +89 -0
- package/src/modules/shipment_tracking/commands/index.ts +8 -0
- package/src/modules/shipment_tracking/commands/location-overrides.ts +119 -0
- package/src/modules/shipment_tracking/commands/shipments.ts +247 -0
- package/src/modules/shipment_tracking/commands/shipsgo-configs.ts +101 -0
- package/src/modules/shipment_tracking/commands/tracking-jobs.ts +158 -0
- package/src/modules/shipment_tracking/commands/tracking.ts +230 -0
- package/src/modules/shipment_tracking/commands/webhooks.ts +159 -0
- package/src/modules/shipment_tracking/components/CombinedTimestampCell.tsx +236 -0
- package/src/modules/shipment_tracking/components/FacilityLocationInput.tsx +235 -0
- package/src/modules/shipment_tracking/components/LocationOverrideDrawer.tsx +586 -0
- package/src/modules/shipment_tracking/components/ShipmentDetailsDrawer.tsx +1364 -0
- package/src/modules/shipment_tracking/components/ShipmentDrawer.tsx +498 -0
- package/src/modules/shipment_tracking/components/ShipsGoConfigSection.tsx +266 -0
- package/src/modules/shipment_tracking/components/TimestampCell.tsx +146 -0
- package/src/modules/shipment_tracking/components/TrackingJobDrawer.tsx +467 -0
- package/src/modules/shipment_tracking/components/VesselTrackingMap.tsx +761 -0
- package/src/modules/shipment_tracking/components/WebhookDrawer.tsx +434 -0
- package/src/modules/shipment_tracking/data/entities.ts +748 -0
- package/src/modules/shipment_tracking/data/validators.ts +523 -0
- package/src/modules/shipment_tracking/di.ts +73 -0
- package/src/modules/shipment_tracking/encryption.ts +17 -0
- package/src/modules/shipment_tracking/events.ts +122 -0
- package/src/modules/shipment_tracking/hooks/useVesselTracking.ts +297 -0
- package/src/modules/shipment_tracking/i18n/de.json +327 -0
- package/src/modules/shipment_tracking/i18n/en.json +327 -0
- package/src/modules/shipment_tracking/i18n/es.json +327 -0
- package/src/modules/shipment_tracking/i18n/pl.json +327 -0
- package/src/modules/shipment_tracking/index.ts +13 -0
- package/src/modules/shipment_tracking/lib/__tests__/dcsa-event-mapping.test.ts +385 -0
- package/src/modules/shipment_tracking/lib/__tests__/dcsa-event-parser.test.ts +398 -0
- package/src/modules/shipment_tracking/lib/__tests__/empty-return.test.ts +174 -0
- package/src/modules/shipment_tracking/lib/__tests__/poi-event-processor.test.ts +699 -0
- package/src/modules/shipment_tracking/lib/__tests__/route-inference.test.ts +402 -0
- package/src/modules/shipment_tracking/lib/__tests__/status-machine.test.ts +447 -0
- package/src/modules/shipment_tracking/lib/__tests__/time-extraction.test.ts +331 -0
- package/src/modules/shipment_tracking/lib/__tests__/timestamp-utils.test.ts +260 -0
- package/src/modules/shipment_tracking/lib/__tests__/vessel-api.test.ts +125 -0
- package/src/modules/shipment_tracking/lib/__tests__/vessel-extraction.test.ts +108 -0
- package/src/modules/shipment_tracking/lib/adapters/cma-cgm.ts +84 -0
- package/src/modules/shipment_tracking/lib/adapters/cosco-types.ts +145 -0
- package/src/modules/shipment_tracking/lib/adapters/cosco.ts +314 -0
- package/src/modules/shipment_tracking/lib/adapters/evergreen.ts +107 -0
- package/src/modules/shipment_tracking/lib/adapters/hapag-lloyd.ts +88 -0
- package/src/modules/shipment_tracking/lib/adapters/index.ts +18 -0
- package/src/modules/shipment_tracking/lib/adapters/maersk.ts +104 -0
- package/src/modules/shipment_tracking/lib/adapters/msc.ts +223 -0
- package/src/modules/shipment_tracking/lib/adapters/zim.ts +106 -0
- package/src/modules/shipment_tracking/lib/audit-helpers.ts +29 -0
- package/src/modules/shipment_tracking/lib/auth/base64url.ts +4 -0
- package/src/modules/shipment_tracking/lib/auth/oauth-client.ts +53 -0
- package/src/modules/shipment_tracking/lib/background.ts +24 -0
- package/src/modules/shipment_tracking/lib/bic-api-client.ts +199 -0
- package/src/modules/shipment_tracking/lib/carrier-adapter.ts +136 -0
- package/src/modules/shipment_tracking/lib/current-vessel.ts +274 -0
- package/src/modules/shipment_tracking/lib/dcsa-event-mapping.ts +202 -0
- package/src/modules/shipment_tracking/lib/dcsa-event-parser.ts +163 -0
- package/src/modules/shipment_tracking/lib/dcsa-params.ts +14 -0
- package/src/modules/shipment_tracking/lib/empty-return.ts +122 -0
- package/src/modules/shipment_tracking/lib/location-overrides.ts +150 -0
- package/src/modules/shipment_tracking/lib/location-types.ts +154 -0
- package/src/modules/shipment_tracking/lib/logger.ts +195 -0
- package/src/modules/shipment_tracking/lib/poi-event-processor.ts +302 -0
- package/src/modules/shipment_tracking/lib/poi-nats-config.ts +134 -0
- package/src/modules/shipment_tracking/lib/poi-types.ts +198 -0
- package/src/modules/shipment_tracking/lib/rate-limiter.ts +58 -0
- package/src/modules/shipment_tracking/lib/route-extraction.ts +439 -0
- package/src/modules/shipment_tracking/lib/route-inference.ts +238 -0
- package/src/modules/shipment_tracking/lib/schedule-generator.ts +92 -0
- package/src/modules/shipment_tracking/lib/shipsgo-client.ts +350 -0
- package/src/modules/shipment_tracking/lib/shipsgo-mapper.ts +296 -0
- package/src/modules/shipment_tracking/lib/status-machine.ts +230 -0
- package/src/modules/shipment_tracking/lib/time-extraction.ts +85 -0
- package/src/modules/shipment_tracking/lib/timestamp-utils.ts +244 -0
- package/src/modules/shipment_tracking/lib/vessel-api.ts +299 -0
- package/src/modules/shipment_tracking/lib/vessel-cache.ts +102 -0
- package/src/modules/shipment_tracking/lib/vessel-extraction.ts +39 -0
- package/src/modules/shipment_tracking/lib/webhook-dispatcher.ts +93 -0
- package/src/modules/shipment_tracking/migrations/.snapshot-open-mercato.json +2914 -0
- package/src/modules/shipment_tracking/migrations/Migration20260422161208_shipment_tracking.ts +60 -0
- package/src/modules/shipment_tracking/migrations/Migration20260529120000_shipment_tracking_poi_region_names.ts +22 -0
- package/src/modules/shipment_tracking/migrations/Migration20260821120000_shipment_tracking_shipsgo.ts +65 -0
- package/src/modules/shipment_tracking/notifications.ts +151 -0
- package/src/modules/shipment_tracking/services/carrierRegistry.ts +29 -0
- package/src/modules/shipment_tracking/services/poiNatsService.ts +526 -0
- package/src/modules/shipment_tracking/services/shipsGoProvider.ts +231 -0
- package/src/modules/shipment_tracking/services/trackingService.ts +1777 -0
- package/src/modules/shipment_tracking/services/webhookService.ts +347 -0
- package/src/modules/shipment_tracking/setup.ts +99 -0
- package/src/modules/shipment_tracking/subscribers/__tests__/terminal-event-ingest.test.ts +141 -0
- package/src/modules/shipment_tracking/subscribers/cargo-event-created.ts +115 -0
- package/src/modules/shipment_tracking/subscribers/dcsa-event-webhook-dispatch.ts +115 -0
- package/src/modules/shipment_tracking/subscribers/poi-event-notification.ts +144 -0
- package/src/modules/shipment_tracking/subscribers/poi-event-webhook-dispatch.ts +94 -0
- package/src/modules/shipment_tracking/subscribers/shipment-status-changed.ts +41 -0
- package/src/modules/shipment_tracking/subscribers/terminal-event-ingest.ts +109 -0
- package/src/modules/shipment_tracking/subscribers/tracking-job-initial-poll.ts +53 -0
- package/src/modules/shipment_tracking/subscribers/tracking-job-webhook-dispatch.ts +50 -0
- package/src/modules/shipment_tracking/tables/index.ts +4 -0
- package/src/modules/shipment_tracking/tables/tracking_job/table.client.tsx +337 -0
- package/src/modules/shipment_tracking/tables/tracking_job/table.ts +34 -0
- package/src/modules/shipment_tracking/widgets/dashboard/ships-map/config.ts +32 -0
- package/src/modules/shipment_tracking/widgets/dashboard/ships-map/widget.client.tsx +448 -0
- package/src/modules/shipment_tracking/widgets/dashboard/ships-map/widget.ts +25 -0
- package/src/types/assets.d.ts +5 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/shipment_tracking/services/trackingService.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { EventBus } from '@open-mercato/events'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Shipment, TrackingJob, TrackingEvent, CarrierConfig, BicConfig } from '../data/entities'\nimport type { TrackingReferenceType, TrackingJobStatusEnum, ShipmentStatusEnum, FacilityCodeListProvider, TrackingProvider, TrackingMode } from '../data/entities'\nimport type { CarrierRegistryService } from './carrierRegistry'\nimport type { ShipsGoProvider } from './shipsGoProvider'\nimport type { WebhookService } from './webhookService'\nimport { ShipsGoCreditsExhaustedError, ShipsGoAuthError, ShipsGoRateLimitedError } from '../lib/shipsgo-client'\nimport type { CacheService } from '../lib/rate-limiter'\nimport { checkRateLimit } from '../lib/rate-limiter'\nimport { trackingLogger, withCarrierContext } from '../lib/logger'\nimport { deriveShipmentStatus, deriveAirShipmentStatus } from '../lib/status-machine'\nimport { extractShipmentTimes } from '../lib/time-extraction'\nimport { generatePollSchedule, getNextPollDate } from '../lib/schedule-generator'\nimport type { CarrierFetchedEvent } from '../lib/carrier-adapter'\nimport { mapDcsaEventToWebhookType, isSignificantMilestone } from '../lib/dcsa-event-mapping'\nimport { inferRouteFromEvents } from '../lib/route-inference'\nimport { allContainersComplete, destinationArrivedBefore, staleArrivalCutoff } from '../lib/empty-return'\nimport { mergeExtractedTimestamps, getPrimaryTimestampValue } from '../lib/timestamp-utils'\nimport { findLatestVesselInfo } from '../lib/vessel-extraction'\nimport { extractRouteFromEvents, mapTrackingEventToEntry } from '../lib/route-extraction'\nimport { buildLocationFromEvent, createBasicLocation, mergeLocationWithBicData, isLocationComplete } from '../lib/location-types'\nimport type { FacilityLocation } from '../lib/location-types'\nimport { BicApiClient, type BicFacility } from '../lib/bic-api-client'\nimport { applyLocationOverrideIfExists } from '../lib/location-overrides'\n\n/**\n * Provider-agnostic summary of a poll's top-level references, threaded from the\n * fetch into shipment derivation. Ocean carries vessel/booking/BOL; air (ShipsGo\n * mode='air') carries the AWB, flight, airline and ShipsGo's top-level status.\n */\ntype CarrierResultSummary = {\n vesselName?: string | null\n vesselImo?: string | null\n bookingNumber?: string | null\n bolNumber?: string | null\n awbNumber?: string | null\n flightNumber?: string | null\n airlineCode?: string | null\n airlineName?: string | null\n airStatus?: string | null\n /** Provider shipment-level metadata merged into Shipment.extra (ShipsGo route / air cargo). */\n extra?: Record<string, unknown> | null\n}\n\ntype TrackingServiceDeps = {\n em: () => EntityManager\n eventBus: EventBus\n carrierRegistry: CarrierRegistryService\n shipsGoProvider: ShipsGoProvider\n cacheService: CacheService\n webhookService: WebhookService\n}\n\nexport class TrackingService {\n private deps: TrackingServiceDeps\n\n constructor(deps: TrackingServiceDeps) {\n this.deps = deps\n }\n\n /**\n * Creates a tracking job and immediately polls the carrier.\n * This is the main entry point for new tracking requests.\n * \n * The system will:\n * 1. Create a TrackingJob for the given carrier and reference\n * 2. Poll the carrier API for events\n * 3. Auto-discover containers from EQUIPMENT events and create Shipments\n * 4. Auto-infer origin/destination ports from events if not provided\n * 5. Emit events for new cargo events and shipment status changes\n */\n async createTrackingJob(input: {\n organizationId: string\n tenantId: string\n carrierCode: string\n referenceType: TrackingReferenceType\n referenceValue: string\n // Provider defaults to 'carrier' (direct DCSA adapter). 'shipsgo' routes\n // through the aggregator fallback and does not require a local adapter.\n provider?: TrackingProvider\n // Transport mode; defaults to 'ocean'.\n mode?: TrackingMode\n // Origin/destination are optional - will be auto-inferred from events if not provided\n originUnlocode?: string\n destinationUnlocode?: string\n schedule?: string[]\n }): Promise<{\n trackingJobId: string\n shipmentsCreated: number\n newEvents: number\n }> {\n const em = this.deps.em()\n const { organizationId, tenantId, carrierCode, referenceType, referenceValue, schedule } = input\n const provider: TrackingProvider = input.provider ?? 'carrier'\n const mode: TrackingMode = input.mode ?? 'ocean'\n // Origin/destination may be provided or will be auto-inferred from events\n let originUnlocode = input.originUnlocode?.toUpperCase()\n let destinationUnlocode = input.destinationUnlocode?.toUpperCase()\n\n const carrierCodeLower = carrierCode.toLowerCase()\n\n // Direct-carrier jobs require a registered adapter; ShipsGo (aggregator)\n // jobs do not \u2014 they are register-then-poll and carrier-agnostic for ocean.\n if (provider === 'carrier' && !this.deps.carrierRegistry.has(carrierCodeLower)) {\n throw new Error(`No adapter registered for carrier: ${carrierCode}`)\n }\n\n // Check for existing active job with same reference\n const existingJob = await em.findOne(TrackingJob, {\n organizationId,\n tenantId,\n carrierCode: carrierCodeLower,\n referenceType,\n referenceValue,\n status: { $in: ['active', 'paused'] },\n deletedAt: null,\n })\n\n if (existingJob) {\n trackingLogger.debug('TrackingJob already exists', {\n trackingJobId: existingJob.id,\n carrierCode: carrierCodeLower,\n referenceType,\n referenceValue,\n })\n // Poll the existing job\n const pollResult = await this.pollTrackingJob(existingJob.id)\n return {\n trackingJobId: existingJob.id,\n shipmentsCreated: pollResult.shipmentsCreated,\n newEvents: pollResult.newEvents,\n }\n }\n\n // Generate poll schedule\n const pollSchedule = schedule ?? generatePollSchedule({})\n const nextPollAt = getNextPollDate(pollSchedule)\n\n // Create new tracking job (origin/destination may be null, will be inferred from first poll)\n const trackingJob = em.create(TrackingJob, {\n organizationId,\n tenantId,\n provider,\n mode,\n carrierCode: carrierCodeLower,\n referenceType,\n referenceValue,\n originUnlocode: originUnlocode ?? null,\n destinationUnlocode: destinationUnlocode ?? null,\n status: 'active',\n schedule: pollSchedule,\n nextPollAt,\n })\n\n em.persist(trackingJob)\n await em.flush()\n\n trackingLogger.info('Created TrackingJob', {\n trackingJobId: trackingJob.id,\n carrierCode: carrierCodeLower,\n referenceType,\n referenceValue,\n tenantId,\n organizationId,\n })\n\n // Emit tracking job created event\n await this.deps.eventBus.emit('shipment_tracking.tracking_job.created', {\n id: trackingJob.id,\n carrierCode: carrierCodeLower,\n referenceType,\n referenceValue,\n tenantId,\n organizationId,\n })\n\n // Poll immediately\n let pollResult = { shipmentsCreated: 0, newEvents: 0 }\n try {\n pollResult = await this.pollTrackingJob(trackingJob.id)\n } catch (error) {\n trackingLogger.error('Initial poll failed', {\n trackingJobId: trackingJob.id,\n carrierCode: carrierCodeLower,\n error: error instanceof Error ? error.message : 'Unknown error',\n })\n\n await this.deps.eventBus.emit('shipment_tracking.tracking_job.poll_failed', {\n id: trackingJob.id,\n carrierCode: carrierCodeLower,\n error: error instanceof Error ? error.message : 'Unknown error',\n tenantId,\n organizationId,\n })\n }\n\n return {\n trackingJobId: trackingJob.id,\n shipmentsCreated: pollResult.shipmentsCreated,\n newEvents: pollResult.newEvents,\n }\n }\n\n /**\n * Polls all active tracking jobs for a tenant/organization.\n * Called by the scheduler for periodic re-polling.\n */\n async pollAllActiveJobs(\n tenantId: string,\n organizationId?: string,\n ): Promise<{ polled: number; newEvents: number; failed: number }> {\n const em = this.deps.em()\n\n const filter: Record<string, unknown> = {\n tenantId,\n status: 'active',\n deletedAt: null,\n }\n\n if (organizationId) {\n filter.organizationId = organizationId\n }\n\n const jobs = await em.find(TrackingJob, filter)\n\n let polled = 0\n let totalNewEvents = 0\n let failed = 0\n\n for (const job of jobs) {\n try {\n const result = await this.pollTrackingJob(job.id)\n polled++\n totalNewEvents += result.newEvents\n } catch (error) {\n failed++\n trackingLogger.error('Poll failed for job', {\n trackingJobId: job.id,\n carrierCode: job.carrierCode,\n error: error instanceof Error ? error.message : 'Unknown error',\n })\n\n await this.deps.eventBus.emit('shipment_tracking.tracking_job.poll_failed', {\n id: job.id,\n carrierCode: job.carrierCode,\n error: error instanceof Error ? error.message : 'Unknown error',\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n })\n }\n }\n\n trackingLogger.info('Poll all completed', {\n polled,\n totalNewEvents,\n failed,\n tenantId,\n organizationId,\n })\n\n return { polled, newEvents: totalNewEvents, failed }\n }\n\n /**\n * Polls a single tracking job:\n * 1. Fetches events from the carrier\n * 2. Persists new TrackingEvents\n * 3. Auto-discovers containers and creates/updates Shipments\n * 4. Emits events for cargo events and shipment changes\n */\n async pollTrackingJob(jobId: string): Promise<{\n newEvents: number\n shipmentsCreated: number\n shipmentsUpdated: number\n }> {\n const em = this.deps.em()\n const job = await em.findOne(TrackingJob, { id: jobId }, { populate: ['shipments'] })\n\n if (!job) {\n throw new Error(`TrackingJob not found: ${jobId}`)\n }\n\n if (job.status !== 'active') {\n return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 }\n }\n\n // Tenant scope \u2014 used for the fetch below and for BIC enrichment later.\n const scope = { tenantId: job.tenantId, organizationId: job.organizationId }\n\n // Fetch events. Two provider paths converge on the same downstream pipeline:\n // 'carrier' \u2192 direct DCSA adapter (rate-limited, config-driven auth)\n // 'shipsgo' \u2192 aggregator, register-then-poll (no local adapter/config)\n let fetchedEvents: CarrierFetchedEvent[]\n let carrierResult: CarrierResultSummary\n\n if (job.provider === 'shipsgo') {\n // Resolve config + rate limit in one config read.\n const pollContext = await this.deps.shipsGoProvider.resolvePollContext(scope, job.mode)\n // Per-tenant rate limit \u2014 ShipsGo caps at 100 req/min account-wide, so a\n // bulk import of unsupported/air legs could otherwise be throttled (and\n // waste credits). The identity is per-tenant for a BYO token and shared\n // for the platform account, so env-fallback tenants share one budget.\n // Skip this tick when over the limit; the schedule retries it, and the job\n // is left untouched (no error, no retry burn).\n const limitResult = await checkRateLimit(\n this.deps.cacheService,\n pollContext.rateLimitIdentity,\n 'shipsgo',\n pollContext.rateLimit.requests,\n pollContext.rateLimit.windowSeconds,\n )\n if (!limitResult.allowed) {\n trackingLogger.debug('ShipsGo rate limited, skipping poll', {\n trackingJobId: jobId,\n retryAfterSeconds: limitResult.retryAfterSeconds,\n })\n return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 }\n }\n\n try {\n const result = await this.deps.shipsGoProvider.fetchOrRegister(job, pollContext.config)\n fetchedEvents = result.events\n carrierResult = {\n vesselName: result.vesselName,\n vesselImo: result.vesselImo,\n bookingNumber: result.bookingNumber,\n bolNumber: result.bolNumber,\n awbNumber: result.awbNumber,\n flightNumber: result.flightNumber,\n airlineCode: result.airlineCode,\n airlineName: result.airlineName,\n airStatus: result.airStatus,\n extra: result.extra,\n }\n } catch (error) {\n // Credit exhaustion (HTTP 402) is an account-level condition, not a\n // per-job fault. Routing it through recordJobError would burn the\n // 10-strike failure budget and flip healthy jobs to 'failed' after ~10\n // polls \u2014 while re-POSTing to an exhausted account each time. Instead\n // back the job off and leave retryCount untouched, so it resumes on its\n // own once credits are topped up.\n if (error instanceof ShipsGoCreditsExhaustedError) {\n await this.recordCreditExhaustion(em, job)\n return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 }\n }\n // 429 (rate limit) and 403 (auth) are transient/account-level like 402:\n // back the job off without burning its retry budget so it recovers on\n // its own once the window resets / the token is fixed.\n if (error instanceof ShipsGoRateLimitedError) {\n const backoffMs = (error.retryAfterSeconds ?? 60) * 1000\n await this.recordTransientBackoff(em, job, error.message, backoffMs)\n return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 }\n }\n if (error instanceof ShipsGoAuthError) {\n await this.recordTransientBackoff(em, job, error.message, 30 * 60 * 1000)\n return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 }\n }\n const message = error instanceof Error ? error.message : 'Unknown ShipsGo fetch error'\n await this.recordJobError(em, job, message)\n return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 }\n }\n } else {\n // Resolve carrier adapter\n const adapter = this.deps.carrierRegistry.get(job.carrierCode)\n if (!adapter) {\n await this.recordJobError(em, job, `No adapter registered for carrier: ${job.carrierCode}`)\n return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 }\n }\n\n // Load carrier config for rate limiting and auth\n const carrierConfig = await findOneWithDecryption(\n em,\n CarrierConfig,\n {\n carrierCode: job.carrierCode,\n organizationId: job.organizationId,\n tenantId: job.tenantId,\n isActive: true,\n },\n undefined,\n scope,\n )\n\n // Check rate limit\n if (carrierConfig) {\n const limitResult = await checkRateLimit(\n this.deps.cacheService,\n job.tenantId,\n job.carrierCode,\n carrierConfig.rateLimitRequests,\n carrierConfig.rateLimitWindowSeconds,\n )\n\n if (!limitResult.allowed) {\n trackingLogger.debug('Rate limited, skipping poll', {\n trackingJobId: jobId,\n carrierCode: job.carrierCode,\n retryAfterSeconds: limitResult.retryAfterSeconds,\n })\n return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 }\n }\n }\n\n try {\n const result = await adapter.fetchEvents({\n referenceType: job.referenceType,\n referenceValue: job.referenceValue,\n apiEndpoint: carrierConfig?.apiEndpoint,\n authConfig: carrierConfig?.authConfig,\n })\n fetchedEvents = result.events\n carrierResult = {\n vesselName: result.vesselName,\n vesselImo: result.vesselImo,\n bookingNumber: result.bookingNumber,\n bolNumber: result.bolNumber,\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown fetch error'\n await this.recordJobError(em, job, message)\n return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 }\n }\n }\n\n // Auto-infer origin/destination from events if not already set\n if (fetchedEvents.length > 0 && (!job.originUnlocode || !job.destinationUnlocode)) {\n const inferred = inferRouteFromEvents(fetchedEvents)\n\n if (!job.originUnlocode && inferred.originUnlocode) {\n job.originUnlocode = inferred.originUnlocode\n trackingLogger.info('Auto-inferred origin', {\n trackingJobId: job.id,\n carrierCode: job.carrierCode,\n origin: inferred.originUnlocode,\n confidence: inferred.confidence.origin,\n })\n }\n\n if (!job.destinationUnlocode && inferred.destinationUnlocode) {\n job.destinationUnlocode = inferred.destinationUnlocode\n trackingLogger.info('Auto-inferred destination', {\n trackingJobId: job.id,\n carrierCode: job.carrierCode,\n destination: inferred.destinationUnlocode,\n confidence: inferred.confidence.destination,\n })\n }\n }\n\n // Persist new events (deduplicate by source + sourceEventId).\n // Estimated (EST) movements reuse a timestamp-independent sourceEventId (see\n // the ShipsGo mapper), so a shifting estimate maps to the SAME row: refresh\n // its timestamp in place rather than skipping (stale ETA) or inserting a new\n // row every poll (unbounded churn + per-poll event noise). Actuals are\n // immutable, so an existing ACT row is simply skipped.\n const existingByKey = new Map(\n (\n await em.find(TrackingEvent, {\n trackingJob: job,\n sourceEventId: { $in: fetchedEvents.map((e) => e.sourceEventId) },\n })\n ).map((e) => [`${e.source}:${e.sourceEventId}`, e]),\n )\n\n const newEvents: TrackingEvent[] = []\n let estRefreshed = false\n for (const fetched of fetchedEvents) {\n const dedupeKey = `${fetched.source}:${fetched.sourceEventId}`\n const prior = existingByKey.get(dedupeKey)\n if (prior) {\n // Refresh a moving estimate in place; leave actuals untouched.\n if (\n fetched.eventClassifierCode === 'EST' &&\n prior.eventDateTime.getTime() !== fetched.eventDateTime.getTime()\n ) {\n prior.eventDateTime = fetched.eventDateTime\n prior.eventDateTimeOffset = fetched.eventDateTimeOffset ?? prior.eventDateTimeOffset\n prior.description = fetched.description ?? prior.description\n prior.rawData = fetched.rawData ?? prior.rawData\n estRefreshed = true\n }\n continue\n }\n\n const trackingEvent = em.create(TrackingEvent, {\n organizationId: job.organizationId,\n tenantId: job.tenantId,\n trackingJob: job,\n\n // Source identification\n source: fetched.source,\n sourceEventId: fetched.sourceEventId,\n\n // Core event fields\n eventType: fetched.eventType,\n eventCode: fetched.eventCode,\n eventClassifierCode: fetched.eventClassifierCode,\n eventDateTime: fetched.eventDateTime,\n eventDateTimeOffset: fetched.eventDateTimeOffset,\n description: fetched.description,\n rawData: fetched.rawData,\n\n // Equipment fields\n equipmentReference: fetched.equipmentReference,\n isoEquipmentCode: fetched.isoEquipmentCode,\n emptyIndicatorCode: fetched.emptyIndicatorCode,\n isTransshipmentMove: fetched.isTransshipmentMove,\n\n // Location fields\n locationName: fetched.locationName,\n locationUnlocode: fetched.locationUnlocode,\n locationCountry: fetched.locationCountry,\n facilityCode: fetched.facilityCode,\n facilityCodeListProvider: fetched.facilityCodeListProvider,\n facilityTypeCode: fetched.facilityTypeCode,\n facilityAddress: fetched.facilityAddress,\n latitude: fetched.latitude,\n longitude: fetched.longitude,\n\n // Transport call fields\n transportCallReference: fetched.transportCallReference,\n modeOfTransport: fetched.modeOfTransport,\n vesselName: fetched.vesselName,\n vesselImo: fetched.vesselImo,\n voyageNumber: fetched.voyageNumber,\n carrierServiceCode: fetched.carrierServiceCode,\n carrierExportVoyageNumber: fetched.carrierExportVoyageNumber,\n carrierImportVoyageNumber: fetched.carrierImportVoyageNumber,\n universalServiceReference: fetched.universalServiceReference,\n universalExportVoyageReference: fetched.universalExportVoyageReference,\n universalImportVoyageReference: fetched.universalImportVoyageReference,\n portVisitReference: fetched.portVisitReference,\n\n // Document references\n relatedDocumentReferences: fetched.relatedDocumentReferences,\n\n // Metadata fields\n eventCreatedDateTime: fetched.eventCreatedDateTime,\n retractedEventId: fetched.retractedEventId,\n publisherName: fetched.publisherName,\n publisherRole: fetched.publisherRole,\n\n // Additional fields\n delayReasonCode: fetched.delayReasonCode,\n changeRemark: fetched.changeRemark,\n seals: fetched.seals,\n })\n newEvents.push(trackingEvent)\n existingByKey.set(dedupeKey, trackingEvent) // Prevent duplicates within same batch\n }\n\n // Persist new events / in-place EST refreshes (if any)\n if (newEvents.length > 0 || estRefreshed) {\n await em.flush()\n }\n\n // Load BIC config for facility enrichment\n const bicConfig = await this.loadBicConfig(em, scope)\n\n // Auto-discover containers and create/update Shipments\n // Always run sync even without new events to backfill missing data (e.g. booking numbers)\n const { shipmentsCreated, shipmentsUpdated, allComplete } = await this.syncShipmentsFromEvents(\n em,\n job,\n carrierResult,\n bicConfig,\n )\n\n // Update job\n job.lastPollAt = new Date()\n job.retryCount = 0\n await em.flush()\n\n // Emit events for each new tracking event\n for (const event of newEvents) {\n await this.emitTrackingEventCreated(event, job)\n }\n\n // Every tracked container has completed its journey (empty return for DCSA,\n // delivery for COSCO), or the backup fired (arrived at destination long ago)\n // \u2192 stop polling this job (self-heals already-active completed jobs on their\n // next poll).\n if (allComplete && job.status === 'active') {\n await this.stopJobInline(em, job, 'completed')\n trackingLogger.info('Tracking job completed (empty return / delivery, or stale after destination arrival)', {\n trackingJobId: job.id,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n })\n }\n\n return { newEvents: newEvents.length, shipmentsCreated, shipmentsUpdated }\n }\n\n /**\n * Syncs Shipments based on TrackingEvents for a job.\n * Auto-discovers containers from equipmentReference in EQUIPMENT events.\n * For container-based tracking (referenceType='container'), creates exactly one Shipment.\n * For booking/BOL tracking, creates one Shipment per unique equipmentReference.\n */\n /**\n * Air counterpart to syncShipmentsFromEvents. Air has no container/equipment\n * model, so it maintains exactly one AWB-keyed Shipment (mode='air') per job,\n * fills flight/airline metadata, and derives timestamps + status from the air\n * movements (via deriveShipmentStateFromEvents with the ShipsGo top-level\n * airStatus). The job auto-completes once the shipment is DELIVERED.\n */\n private async syncAirShipmentFromEvents(\n em: EntityManager,\n job: TrackingJob,\n carrierResult: CarrierResultSummary,\n bicConfig: BicConfig | null,\n ): Promise<{ shipmentsCreated: number; shipmentsUpdated: number; allComplete: boolean }> {\n const allEvents = await em.find(\n TrackingEvent,\n { trackingJob: job },\n { orderBy: { eventDateTime: 'asc' } },\n )\n\n const existing = await em.find(Shipment, { trackingJob: job, deletedAt: null })\n let shipment = existing[0]\n let shipmentsCreated = 0\n let isNew = false\n\n if (!shipment) {\n shipment = em.create(Shipment, {\n organizationId: job.organizationId,\n tenantId: job.tenantId,\n trackingJob: job,\n carrierCode: job.carrierCode,\n mode: 'air',\n awbNumber: carrierResult.awbNumber ?? job.referenceValue,\n originLocation: job.originUnlocode ? createBasicLocation(job.originUnlocode) : null,\n destinationLocation: job.destinationUnlocode ? createBasicLocation(job.destinationUnlocode) : null,\n status: deriveAirShipmentStatus(carrierResult.airStatus, 'PENDING') as ShipmentStatusEnum,\n })\n em.persist(shipment)\n shipmentsCreated = 1\n isNew = true\n trackingLogger.info('Auto-created air Shipment', {\n shipmentId: shipment.id,\n awbNumber: shipment.awbNumber,\n trackingJobId: job.id,\n })\n }\n\n // Latest flight/airline metadata wins (backfill on new, refresh on existing).\n if (carrierResult.flightNumber) shipment.flightNumber = carrierResult.flightNumber\n if (carrierResult.airlineCode) shipment.airlineCode = carrierResult.airlineCode\n if (carrierResult.airlineName) shipment.airlineName = carrierResult.airlineName\n if (!shipment.awbNumber && carrierResult.awbNumber) shipment.awbNumber = carrierResult.awbNumber\n // Merge provider shipment-level metadata (ShipsGo air cargo / status_extended) into extra.\n if (carrierResult.extra) {\n shipment.extra = { ...(shipment.extra ?? {}), ...carrierResult.extra }\n }\n\n const statusChanges: Array<{ shipment: Shipment; previousStatus: string; newStatus: string }> = []\n const etaChanges: Array<{ shipment: Shipment; previousEta: Date; newEta: Date }> = []\n const etdChanges: Array<{ shipment: Shipment; previousEtd: Date; newEtd: Date }> = []\n\n let shipmentsUpdated = 0\n if (allEvents.length > 0) {\n const result = await this.deriveShipmentStateFromEvents(\n em, shipment, allEvents, bicConfig, carrierResult.airStatus ?? null,\n )\n if (result.changed) {\n shipmentsUpdated = 1\n if (result.statusChange) statusChanges.push({ shipment, ...result.statusChange })\n if (result.etaChange) etaChanges.push({ shipment, ...result.etaChange })\n if (result.etdChange) etdChanges.push({ shipment, ...result.etdChange })\n }\n } else if (carrierResult.airStatus) {\n // No movements yet \u2014 still reflect the ShipsGo top-level status.\n const next = deriveAirShipmentStatus(carrierResult.airStatus, shipment.status)\n if (next !== shipment.status) {\n statusChanges.push({ shipment, previousStatus: shipment.status, newStatus: next })\n shipment.status = next as ShipmentStatusEnum\n shipmentsUpdated = 1\n }\n }\n\n await em.flush()\n\n if (isNew) {\n await this.deps.eventBus.emit('shipment_tracking.shipment.created', {\n id: shipment.id,\n awbNumber: shipment.awbNumber,\n trackingJobId: job.id,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n })\n }\n await this.emitShipmentDerivedChanges([shipment], statusChanges, etaChanges, etdChanges)\n\n // Air completes on DELIVERED, or \u2014 the backup, mirroring the ocean stale\n // cutoff \u2014 once it has ARRIVED and its latest movement is older than\n // STALE_AFTER_ARRIVAL_DAYS. Air last-mile delivery is frequently untracked,\n // so without this an ARRIVED-but-never-DELIVERED shipment would poll forever.\n const latestEventAt = allEvents.length > 0 ? allEvents[allEvents.length - 1].eventDateTime : null\n const staleArrived =\n shipment.status === 'ARRIVED' && latestEventAt != null && latestEventAt < staleArrivalCutoff()\n\n return {\n shipmentsCreated,\n shipmentsUpdated,\n allComplete: shipment.status === 'DELIVERED' || staleArrived,\n }\n }\n\n private async syncShipmentsFromEvents(\n em: EntityManager,\n job: TrackingJob,\n carrierResult: CarrierResultSummary,\n bicConfig: BicConfig | null,\n ): Promise<{ shipmentsCreated: number; shipmentsUpdated: number; allComplete: boolean }> {\n // Air jobs have no container/equipment model \u2014 one AWB-keyed Shipment.\n if (job.mode === 'air') {\n return this.syncAirShipmentFromEvents(em, job, carrierResult, bicConfig)\n }\n\n // Get all events for this job\n const allEvents = await em.find(\n TrackingEvent,\n { trackingJob: job },\n { orderBy: { eventDateTime: 'asc' } },\n )\n\n if (allEvents.length === 0) {\n return { shipmentsCreated: 0, shipmentsUpdated: 0, allComplete: false }\n }\n\n // Extract unique container numbers (equipmentReferences) from EQUIPMENT events\n const containerNumbers = new Set<string>()\n for (const event of allEvents) {\n if (event.eventType === 'EQUIPMENT' && event.equipmentReference) {\n containerNumbers.add(event.equipmentReference)\n }\n }\n\n // For container-based tracking with no equipment events yet, use the reference value\n if (containerNumbers.size === 0 && job.referenceType === 'container') {\n containerNumbers.add(job.referenceValue)\n }\n\n // If still no containers found, nothing to do yet\n if (containerNumbers.size === 0) {\n return { shipmentsCreated: 0, shipmentsUpdated: 0, allComplete: false }\n }\n\n // Load existing shipments for this job\n const existingShipments = await em.find(Shipment, {\n trackingJob: job,\n deletedAt: null,\n })\n const shipmentsByContainer = new Map(\n existingShipments.map((s) => [s.containerNumber, s]),\n )\n\n let shipmentsCreated = 0\n let shipmentsUpdated = 0\n\n // Track shipments that need events emitted after flush\n const newShipments: Array<{ shipment: Shipment; containerNumber: string }> = []\n const statusChanges: Array<{\n shipment: Shipment\n previousStatus: string\n newStatus: string\n }> = []\n const etaChanges: Array<{\n shipment: Shipment\n previousEta: Date\n newEta: Date\n }> = []\n const etdChanges: Array<{\n shipment: Shipment\n previousEtd: Date\n newEtd: Date\n }> = []\n\n // Create or update shipments for each container\n for (const containerNumber of containerNumbers) {\n let shipment = shipmentsByContainer.get(containerNumber)\n\n if (!shipment) {\n // Create new shipment - inherit origin/destination from tracking job\n shipment = em.create(Shipment, {\n organizationId: job.organizationId,\n tenantId: job.tenantId,\n trackingJob: job,\n carrierCode: job.carrierCode,\n containerNumber,\n bookingNumber: job.referenceType === 'booking' ? job.referenceValue : carrierResult.bookingNumber,\n bolNumber: job.referenceType === 'bol' ? job.referenceValue : carrierResult.bolNumber,\n // Initialize with basic location from job's UN/LOCODE if available\n // Will be enriched with full facility data from events later\n originLocation: job.originUnlocode ? createBasicLocation(job.originUnlocode) : null,\n destinationLocation: job.destinationUnlocode ? createBasicLocation(job.destinationUnlocode) : null,\n status: 'PENDING',\n })\n em.persist(shipment)\n shipmentsByContainer.set(containerNumber, shipment)\n shipmentsCreated++\n newShipments.push({ shipment, containerNumber })\n\n trackingLogger.info('Auto-created Shipment', {\n shipmentId: shipment.id,\n containerNumber,\n trackingJobId: job.id,\n carrierCode: job.carrierCode,\n })\n }\n\n // Backfill booking/BOL number from carrier result if still missing\n if (!shipment.bookingNumber && carrierResult.bookingNumber) {\n shipment.bookingNumber = carrierResult.bookingNumber\n }\n if (!shipment.bolNumber && carrierResult.bolNumber) {\n shipment.bolNumber = carrierResult.bolNumber\n }\n // Merge provider shipment-level metadata (ShipsGo route summary) into extra.\n if (carrierResult.extra) {\n shipment.extra = { ...(shipment.extra ?? {}), ...carrierResult.extra }\n }\n\n // Update shipment state from events\n const result = await this.deriveShipmentStateFromEvents(em, shipment, allEvents, bicConfig)\n if (result.changed) {\n shipmentsUpdated++\n if (result.statusChange) {\n statusChanges.push({\n shipment,\n previousStatus: result.statusChange.previousStatus,\n newStatus: result.statusChange.newStatus,\n })\n }\n if (result.etaChange) {\n etaChanges.push({\n shipment,\n previousEta: result.etaChange.previousEta,\n newEta: result.etaChange.newEta,\n })\n }\n if (result.etdChange) {\n etdChanges.push({\n shipment,\n previousEtd: result.etdChange.previousEtd,\n newEtd: result.etdChange.newEtd,\n })\n }\n }\n }\n\n // Flush all changes to database BEFORE emitting events\n await em.flush()\n\n // Now emit events - shipments are committed and can be fetched by subscribers\n for (const { shipment, containerNumber } of newShipments) {\n await this.deps.eventBus.emit('shipment_tracking.shipment.created', {\n id: shipment.id,\n containerNumber,\n trackingJobId: job.id,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n })\n }\n\n // Emit status/timestamp change events (shared with the real-time AIS refresh path)\n const polledShipments = [...containerNumbers]\n .map((cn) => shipmentsByContainer.get(cn))\n .filter((s): s is Shipment => s != null)\n await this.emitShipmentDerivedChanges(polledShipments, statusChanges, etaChanges, etdChanges)\n\n // Whole shipment done once every tracked container has completed its\n // journey (empty return for DCSA carriers, delivery for COSCO), OR \u2014 as a\n // backup when that event never arrives \u2014 once it arrived at its destination\n // port more than STALE_AFTER_ARRIVAL_DAYS ago.\n const allComplete =\n allContainersComplete(allEvents, containerNumbers) ||\n destinationArrivedBefore(allEvents, job.destinationUnlocode, staleArrivalCutoff())\n\n return { shipmentsCreated, shipmentsUpdated, allComplete }\n }\n\n /**\n * Emit the shipment status/lifecycle/timestamp change events produced by a round of\n * deriveShipmentStateFromEvents. Shared by the carrier poll (syncShipmentsFromEvents)\n * and the real-time AIS refresh (refreshShipmentsFromEvents) so both paths surface the\n * same downstream events. Call AFTER flushing so subscribers can fetch committed rows.\n *\n * @param updatedShipments - shipments to emit a bare `shipment.updated` for when they\n * did not also produce a status change (status changes already emit `updated`).\n */\n private async emitShipmentDerivedChanges(\n updatedShipments: Shipment[],\n statusChanges: Array<{ shipment: Shipment; previousStatus: string; newStatus: string }>,\n etaChanges: Array<{ shipment: Shipment; previousEta: Date; newEta: Date }>,\n etdChanges: Array<{ shipment: Shipment; previousEtd: Date; newEtd: Date }>,\n ): Promise<void> {\n for (const { shipment, previousStatus, newStatus } of statusChanges) {\n await this.deps.eventBus.emit('shipment_tracking.shipment.status_changed', {\n id: shipment.id,\n previousStatus,\n newStatus,\n tenantId: shipment.tenantId,\n organizationId: shipment.organizationId,\n })\n\n // Emit specific lifecycle events\n if (newStatus === 'BOOKED' && previousStatus === 'PENDING') {\n await this.deps.eventBus.emit('shipment_tracking.shipment.booked', {\n id: shipment.id,\n tenantId: shipment.tenantId,\n organizationId: shipment.organizationId,\n })\n }\n\n if (newStatus === 'PRE_ARRIVAL') {\n await this.deps.eventBus.emit('shipment_tracking.shipment.pre_arrival', {\n id: shipment.id,\n tenantId: shipment.tenantId,\n organizationId: shipment.organizationId,\n })\n }\n\n if (newStatus === 'DELIVERED') {\n await this.deps.eventBus.emit('shipment_tracking.shipment.delivered', {\n id: shipment.id,\n tenantId: shipment.tenantId,\n organizationId: shipment.organizationId,\n })\n }\n\n await this.deps.eventBus.emit('shipment_tracking.shipment.updated', {\n id: shipment.id,\n tenantId: shipment.tenantId,\n organizationId: shipment.organizationId,\n })\n }\n\n // Emit shipment.updated for shipments that had data changes but no status change\n // (status changes already emit updated above). This ensures cross-module subscribers\n // (e.g., fms_files leg sync) pick up vessel info, timestamps, booking numbers, etc.\n const alreadyEmittedIds = new Set(statusChanges.map((sc) => sc.shipment.id))\n for (const shipment of updatedShipments) {\n if (!alreadyEmittedIds.has(shipment.id)) {\n await this.deps.eventBus.emit('shipment_tracking.shipment.updated', {\n id: shipment.id,\n tenantId: shipment.tenantId,\n organizationId: shipment.organizationId,\n })\n }\n }\n\n // Emit ETA change events\n for (const { shipment, previousEta, newEta } of etaChanges) {\n await this.deps.eventBus.emit('shipment_tracking.transport.eta_updated', {\n id: shipment.id,\n previousEta: previousEta.toISOString(),\n newEta: newEta.toISOString(),\n tenantId: shipment.tenantId,\n organizationId: shipment.organizationId,\n })\n }\n\n // Emit ETD change events\n for (const { shipment, previousEtd, newEtd } of etdChanges) {\n await this.deps.eventBus.emit('shipment_tracking.transport.etd_updated', {\n id: shipment.id,\n previousEtd: previousEtd.toISOString(),\n newEtd: newEtd.toISOString(),\n tenantId: shipment.tenantId,\n organizationId: shipment.organizationId,\n })\n }\n }\n\n /**\n * Re-derive denormalized state (cargoEvents, routeStops, status, ETA/ETD/ATA/ATD, seals,\n * locations) for a tracking job's shipments from their persisted TrackingEvents, then emit\n * the resulting change events. Used by real-time ingestion (e.g. AIS POI events over NATS)\n * so the Journey Timeline and derived state update immediately instead of waiting for the\n * next carrier poll. Idempotent: deriveShipmentStateFromEvents is a no-op when nothing changed.\n *\n * @param em - entity manager managing the (already flushed) new TrackingEvents\n * @param trackingJobId - the job whose shipments to refresh\n * @param shipmentIds - optional subset; defaults to all of the job's shipments\n */\n async refreshShipmentsFromEvents(\n em: EntityManager,\n trackingJobId: string,\n shipmentIds?: string[],\n ): Promise<void> {\n const job = await em.findOne(TrackingJob, { id: trackingJobId })\n if (!job) return\n\n // Air shipments derive status from ShipsGo's top-level air status during the\n // poll (deriveAirShipmentStatus), not from DCSA/AIS vessel events. Running\n // the DCSA re-derive here (no airStatus) would mis-set them. AIS is\n // vessel-only, so an air job should never reach this path \u2014 guard anyway.\n if (job.mode === 'air') return\n\n const shipments = shipmentIds && shipmentIds.length > 0\n ? await em.find(Shipment, { trackingJob: job, deletedAt: null, id: { $in: shipmentIds } })\n : await em.find(Shipment, { trackingJob: job, deletedAt: null })\n if (shipments.length === 0) return\n\n const allEvents = await em.find(\n TrackingEvent,\n { trackingJob: job },\n { orderBy: { eventDateTime: 'asc' } },\n )\n if (allEvents.length === 0) return\n\n const bicConfig = await this.loadBicConfig(em, {\n organizationId: job.organizationId,\n tenantId: job.tenantId,\n })\n\n const statusChanges: Array<{ shipment: Shipment; previousStatus: string; newStatus: string }> = []\n const etaChanges: Array<{ shipment: Shipment; previousEta: Date; newEta: Date }> = []\n const etdChanges: Array<{ shipment: Shipment; previousEtd: Date; newEtd: Date }> = []\n const changedShipments: Shipment[] = []\n\n for (const shipment of shipments) {\n const result = await this.deriveShipmentStateFromEvents(em, shipment, allEvents, bicConfig)\n if (!result.changed) continue\n changedShipments.push(shipment)\n if (result.statusChange) statusChanges.push({ shipment, ...result.statusChange })\n if (result.etaChange) etaChanges.push({ shipment, ...result.etaChange })\n if (result.etdChange) etdChanges.push({ shipment, ...result.etdChange })\n }\n\n if (changedShipments.length === 0) return\n\n await em.flush()\n await this.emitShipmentDerivedChanges(changedShipments, statusChanges, etaChanges, etdChanges)\n }\n\n /**\n * Derives and updates Shipment state from TrackingEvents.\n * Filters events by equipmentReference to get container-specific events.\n * TRANSPORT events (no equipmentReference) apply to all containers.\n * \n * Returns status change info and time changes so caller can emit events after flush.\n */\n private async deriveShipmentStateFromEvents(\n em: EntityManager,\n shipment: Shipment,\n allEvents: TrackingEvent[],\n bicConfig: BicConfig | null,\n airStatus?: string | null,\n ): Promise<{\n changed: boolean\n statusChange?: { previousStatus: string; newStatus: string }\n etaChange?: { previousEta: Date; newEta: Date }\n etdChange?: { previousEtd: Date; newEtd: Date }\n }> {\n // Filter events for this specific container\n // Include EQUIPMENT events with matching equipmentReference\n // Include TRANSPORT events (they apply to all containers)\n const containerEvents = allEvents.filter((event) => {\n if (event.eventType === 'TRANSPORT') return true\n if (event.eventType === 'EQUIPMENT') {\n return event.equipmentReference === shipment.containerNumber\n }\n return false\n })\n\n if (containerEvents.length === 0) {\n return { changed: false }\n }\n\n // Get UN/LOCODEs from JSONB location objects\n const originUnlocode = shipment.originLocation?.unlocode ?? null\n const destinationUnlocode = shipment.destinationLocation?.unlocode ?? null\n \n const context = {\n originUnlocode,\n destinationUnlocode,\n }\n\n // Extract times from events first - needed for both status derivation and updates\n const times = extractShipmentTimes(\n containerEvents.map((event) => ({\n eventCode: event.eventCode,\n eventClassifierCode: event.eventClassifierCode,\n eventDateTime: event.eventDateTime,\n eventDateTimeOffset: event.eventDateTimeOffset,\n eventCreatedDateTime: event.eventCreatedDateTime,\n locationUnlocode: event.locationUnlocode,\n })),\n context,\n )\n\n // Get latest event for lastEventAt\n const latestEvent = containerEvents[containerEvents.length - 1]\n\n // Find latest event with vessel info (gate operations often don't have vessel data)\n const latestVesselInfo = findLatestVesselInfo(containerEvents)\n\n // Track previous primary timestamp values for change detection\n const previousEta = getPrimaryTimestampValue(shipment.etaTimestamps)\n const previousEtd = getPrimaryTimestampValue(shipment.etdTimestamps)\n const previousAtd = getPrimaryTimestampValue(shipment.atdTimestamps)\n const previousAta = getPrimaryTimestampValue(shipment.ataTimestamps)\n\n // Merge extracted timestamps into existing arrays (with deduplication)\n const mergedTimestamps = mergeExtractedTimestamps(\n {\n etdTimestamps: shipment.etdTimestamps,\n etaTimestamps: shipment.etaTimestamps,\n atdTimestamps: shipment.atdTimestamps,\n ataTimestamps: shipment.ataTimestamps,\n },\n times,\n 'carrier_api',\n latestEvent?.sourceEventId,\n )\n\n // Get new primary values after merge\n const newEta = getPrimaryTimestampValue(mergedTimestamps.etaTimestamps)\n const newEtd = getPrimaryTimestampValue(mergedTimestamps.etdTimestamps)\n const newAtd = getPrimaryTimestampValue(mergedTimestamps.atdTimestamps)\n const newAta = getPrimaryTimestampValue(mergedTimestamps.ataTimestamps)\n\n // Derive status. Air (mode='air') has no container/vessel movement model, so\n // its status comes from ShipsGo's top-level status (LANDED\u2192ARRIVED, \u2026) rather\n // than DCSA event-location matching. Ocean/carrier use the DCSA status machine\n // with a time context for the PRE_ARRIVAL upgrade.\n const previousStatus = shipment.status\n const newStatus = airStatus !== undefined\n ? deriveAirShipmentStatus(airStatus, shipment.status)\n : deriveShipmentStatus(\n containerEvents.map((event) => ({\n eventCode: event.eventCode,\n eventClassifierCode: event.eventClassifierCode,\n locationUnlocode: event.locationUnlocode,\n })),\n context,\n shipment.status,\n { eta: newEta, ata: newAta },\n )\n\n // Check if ETA/ETD changed (compare timestamps, handle null)\n const etaChanged = newEta && previousEta && newEta.getTime() !== previousEta.getTime()\n const etdChanged = newEtd && previousEtd && newEtd.getTime() !== previousEtd.getTime()\n\n // Check if anything changed.\n // Includes an event-count delta so that appending events which don't move any derived\n // value (e.g. AIS POI port/terminal events, whose codes aren't read by time/status\n // extraction) still triggers a denormalized cargoEvents/routeStops rebuild \u2014 otherwise\n // they'd never reach the Journey Timeline.\n const changed =\n previousStatus !== newStatus ||\n mergedTimestamps.changed ||\n (newEtd && !previousEtd) || etdChanged ||\n (newEta && !previousEta) || etaChanged ||\n (newAtd && !previousAtd) || (newAtd && previousAtd && newAtd.getTime() !== previousAtd.getTime()) ||\n (newAta && !previousAta) || (newAta && previousAta && newAta.getTime() !== previousAta.getTime()) ||\n shipment.vesselName !== latestEvent?.vesselName ||\n containerEvents.length !== shipment.eventCount\n\n if (!changed) {\n return { changed: false }\n }\n\n // Update shipment\n shipment.status = newStatus as ShipmentStatusEnum\n\n // Update timestamp arrays\n shipment.etdTimestamps = mergedTimestamps.etdTimestamps\n shipment.etaTimestamps = mergedTimestamps.etaTimestamps\n shipment.atdTimestamps = mergedTimestamps.atdTimestamps\n shipment.ataTimestamps = mergedTimestamps.ataTimestamps\n\n // Update vessel info from latest event with vessel data\n if (latestVesselInfo) {\n shipment.vesselName = latestVesselInfo.vesselName ?? shipment.vesselName\n shipment.vesselImo = latestVesselInfo.vesselImo ?? shipment.vesselImo\n shipment.voyageNumber = latestVesselInfo.voyageNumber ?? shipment.voyageNumber\n }\n\n // Extract ISO equipment code from the first EQUIPMENT event that has it\n if (!shipment.isoEquipmentCode) {\n const eventWithIsoCode = containerEvents.find(e => e.isoEquipmentCode)\n if (eventWithIsoCode) {\n shipment.isoEquipmentCode = eventWithIsoCode.isoEquipmentCode\n }\n }\n\n // Extract booking number from document references if not already set\n if (!shipment.bookingNumber) {\n for (const event of containerEvents) {\n const bkgRef = event.relatedDocumentReferences?.find(ref => ref.type === 'BKG')\n if (bkgRef?.value) {\n shipment.bookingNumber = bkgRef.value\n break\n }\n }\n }\n\n // Extract BOL number from document references if not already set\n if (!shipment.bolNumber) {\n for (const event of containerEvents) {\n const bolRef = event.relatedDocumentReferences?.find(ref => ref.type === 'TRD' || ref.type === 'SHI')\n if (bolRef?.value) {\n shipment.bolNumber = bolRef.value\n break\n }\n }\n }\n\n // Update lastEventAt from actual latest event\n if (latestEvent) {\n shipment.lastEventAt = latestEvent.eventDateTime\n }\n\n shipment.eventCount = containerEvents.length\n\n // \u2500\u2500\u2500 Denormalize cargo events and route stops \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Map TrackingEvent entities to CargoEventEntry format for JSONB storage\n const cargoEvents = containerEvents.map(mapTrackingEventToEntry)\n shipment.cargoEvents = cargoEvents\n\n // Extract route stops from the mapped events\n shipment.routeStops = extractRouteFromEvents(cargoEvents, {\n originUnlocode,\n destinationUnlocode,\n })\n\n // \u2500\u2500\u2500 Aggregate seals from all events \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Collect all unique seals seen across all cargo events for this container.\n // Deduplicate by seal number, keeping the most recent occurrence.\n const allSeals = containerEvents\n .flatMap(e => e.seals ?? [])\n .filter(seal => seal.number) // Ensure valid seal with number\n \n if (allSeals.length > 0) {\n // Deduplicate by seal number - later occurrences (newer events) win\n const seenSeals = new Map<string, typeof allSeals[0]>()\n for (const seal of allSeals) {\n seenSeals.set(seal.number, seal)\n }\n shipment.seals = Array.from(seenSeals.values())\n } else {\n shipment.seals = null\n }\n\n // \u2500\u2500\u2500 Build rich origin/destination locations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Find the best event for origin using priority:\n // 1. LOAD event at origin (actual loading at terminal)\n // 2. Any event at origin with facilityCode (terminal data available)\n // 3. Fallback: any event at origin location\n const originEventsAtLocation = containerEvents.filter(e => \n e.locationUnlocode === originUnlocode\n )\n const originEvent = \n // Priority 1: LOAD event at origin\n originEventsAtLocation.find(e => e.eventCode === 'LOAD' && e.eventClassifierCode === 'ACT') ??\n // Priority 2: Any event with facility code at origin\n originEventsAtLocation.find(e => e.facilityCode != null) ??\n // Fallback: First event at origin, or LOAD event anywhere if no origin specified\n originEventsAtLocation[0] ??\n (!originUnlocode ? containerEvents.find(e => e.eventCode === 'LOAD' && e.eventClassifierCode === 'ACT') : null)\n \n // Find the best event for destination using priority:\n // 1. DISC event at destination (actual discharge at terminal)\n // 2. Any event at destination with facilityCode (terminal data available)\n // 3. Fallback: last event at destination location\n const destEventsAtLocation = containerEvents.filter(e => \n e.locationUnlocode === destinationUnlocode\n )\n const destEventsReversed = [...destEventsAtLocation].reverse()\n const destEvent = \n // Priority 1: DISC event at destination (last one)\n destEventsReversed.find(e => e.eventCode === 'DISC' && e.eventClassifierCode === 'ACT') ??\n // Priority 2: Any event with facility code at destination (last one)\n destEventsReversed.find(e => e.facilityCode != null) ??\n // Fallback: Last event at destination, or DISC/ARRI event anywhere if no destination specified\n destEventsReversed[0] ??\n (!destinationUnlocode ? [...containerEvents].reverse().find(e => (e.eventCode === 'DISC' || e.eventCode === 'ARRI') && e.eventClassifierCode === 'ACT') : null)\n\n if (originEvent) {\n shipment.originLocation = buildLocationFromEvent({\n locationName: originEvent.locationName,\n locationUnlocode: originEvent.locationUnlocode,\n locationCountry: originEvent.locationCountry,\n facilityCode: originEvent.facilityCode,\n facilityCodeListProvider: originEvent.facilityCodeListProvider,\n facilityTypeCode: originEvent.facilityTypeCode,\n facilityAddress: originEvent.facilityAddress,\n latitude: originEvent.latitude,\n longitude: originEvent.longitude,\n })\n }\n\n if (destEvent) {\n shipment.destinationLocation = buildLocationFromEvent({\n locationName: destEvent.locationName,\n locationUnlocode: destEvent.locationUnlocode,\n locationCountry: destEvent.locationCountry,\n facilityCode: destEvent.facilityCode,\n facilityCodeListProvider: destEvent.facilityCodeListProvider,\n facilityTypeCode: destEvent.facilityTypeCode,\n facilityAddress: destEvent.facilityAddress,\n latitude: destEvent.latitude,\n longitude: destEvent.longitude,\n })\n }\n\n // \u2500\u2500\u2500 Enrich locations with BIC Facility API data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Only enrich if BIC config is enabled and locations are incomplete\n if (bicConfig) {\n await this.enrichShipmentLocationsWithBic(shipment, containerEvents, bicConfig)\n }\n\n // \u2500\u2500\u2500 Apply location overrides \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Overrides take priority over BIC data and allow correcting incorrect terminal info\n const scope = { organizationId: shipment.organizationId, tenantId: shipment.tenantId }\n const carrierCode = shipment.carrierCode?.toUpperCase() ?? null\n\n shipment.originLocation = await applyLocationOverrideIfExists(\n em, shipment.originLocation, carrierCode, scope\n )\n shipment.destinationLocation = await applyLocationOverrideIfExists(\n em, shipment.destinationLocation, carrierCode, scope\n )\n\n // Apply overrides to route stops\n if (shipment.routeStops) {\n for (const stop of shipment.routeStops) {\n if (stop.facilityCode && stop.facilityCodeListProvider) {\n const overridden = await applyLocationOverrideIfExists(em, {\n name: stop.location,\n unlocode: stop.unlocode ?? null,\n countryCode: stop.unlocode?.slice(0, 2) ?? null,\n facilityCode: stop.facilityCode,\n facilityCodeListProvider: stop.facilityCodeListProvider,\n facilityTypeCode: stop.facilityTypeCode ?? null,\n address: stop.facilityAddress ?? null,\n coords: stop.coords ?? null,\n operatorName: null,\n source: 'dcsa',\n }, carrierCode, scope)\n if (overridden && overridden.source === 'manual') {\n stop.location = overridden.name\n stop.facilityAddress = overridden.address\n stop.coords = overridden.coords\n }\n }\n }\n }\n\n // Return change info so caller can emit events after flush\n const statusChange = previousStatus !== newStatus\n ? { previousStatus, newStatus }\n : undefined\n\n // Only report ETA/ETD changes when the value actually changed (not initial set)\n const etaChange = etaChanged && previousEta && newEta\n ? { previousEta, newEta }\n : undefined\n\n const etdChange = etdChanged && previousEtd && newEtd\n ? { previousEtd, newEtd }\n : undefined\n\n return { changed: true, statusChange, etaChange, etdChange }\n }\n\n /**\n * Emits events for a newly created TrackingEvent.\n */\n private async emitTrackingEventCreated(\n event: TrackingEvent,\n job: TrackingJob,\n ): Promise<void> {\n const eventPayload = {\n id: event.id,\n trackingJobId: job.id,\n tenantId: event.tenantId,\n organizationId: event.organizationId,\n\n // Source\n source: event.source,\n sourceEventId: event.sourceEventId,\n\n // Core event fields\n eventType: event.eventType,\n eventCode: event.eventCode,\n eventClassifierCode: event.eventClassifierCode,\n eventDateTime: event.eventDateTime?.toISOString(),\n description: event.description,\n\n // Equipment fields\n equipmentReference: event.equipmentReference,\n isoEquipmentCode: event.isoEquipmentCode,\n emptyIndicatorCode: event.emptyIndicatorCode,\n isTransshipmentMove: event.isTransshipmentMove,\n\n // Location fields\n locationName: event.locationName,\n locationUnlocode: event.locationUnlocode,\n locationCountry: event.locationCountry,\n facilityCode: event.facilityCode,\n facilityTypeCode: event.facilityTypeCode,\n\n // Transport call fields\n vesselName: event.vesselName,\n vesselImo: event.vesselImo,\n voyageNumber: event.voyageNumber,\n carrierServiceCode: event.carrierServiceCode,\n modeOfTransport: event.modeOfTransport,\n\n // Document references\n relatedDocumentReferences: event.relatedDocumentReferences,\n\n // Metadata\n publisherName: event.publisherName,\n publisherRole: event.publisherRole,\n }\n\n // Always emit generic event for backward compatibility\n await this.deps.eventBus.emit('shipment_tracking.tracking_event.created', eventPayload)\n\n // Emit granular DCSA-compliant event if significant milestone\n if (isSignificantMilestone({\n eventType: event.eventType,\n eventCode: event.eventCode,\n eventClassifierCode: event.eventClassifierCode,\n })) {\n const dcsaEventType = mapDcsaEventToWebhookType({\n eventType: event.eventType,\n eventCode: event.eventCode,\n eventClassifierCode: event.eventClassifierCode,\n })\n\n await this.deps.eventBus.emit(dcsaEventType, eventPayload)\n }\n }\n\n /**\n * Loads BIC Facility API configuration for a tenant/organization.\n */\n private async loadBicConfig(\n em: EntityManager,\n scope: { tenantId: string; organizationId: string },\n ): Promise<BicConfig | null> {\n return findOneWithDecryption(\n em,\n BicConfig,\n {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n isEnabled: true,\n },\n undefined,\n scope,\n )\n }\n\n /**\n * Enriches shipment origin/destination locations and route stops with BIC Facility API data.\n * Only fetches data for facilities that are incomplete (missing coords or address).\n * One API call per unique facility code per poll.\n */\n private async enrichShipmentLocationsWithBic(\n shipment: Shipment,\n events: TrackingEvent[],\n bicConfig: BicConfig,\n ): Promise<void> {\n // Collect unique facility codes that need enrichment\n const facilitiesToEnrich = new Map<string, { code: string; provider: FacilityCodeListProvider; unlocode: string }>()\n\n // Check origin location\n if (shipment.originLocation && !isLocationComplete(shipment.originLocation) && shipment.originLocation.facilityCode) {\n const provider = shipment.originLocation.facilityCodeListProvider ?? 'SMDG'\n facilitiesToEnrich.set(shipment.originLocation.facilityCode, {\n code: shipment.originLocation.facilityCode,\n provider,\n unlocode: shipment.originLocation.unlocode ?? '',\n })\n }\n\n // Check destination location\n if (shipment.destinationLocation && !isLocationComplete(shipment.destinationLocation) && shipment.destinationLocation.facilityCode) {\n const provider = shipment.destinationLocation.facilityCodeListProvider ?? 'SMDG'\n facilitiesToEnrich.set(shipment.destinationLocation.facilityCode, {\n code: shipment.destinationLocation.facilityCode,\n provider,\n unlocode: shipment.destinationLocation.unlocode ?? '',\n })\n }\n\n // Check events for facility codes (for route stops enrichment)\n for (const event of events) {\n if (event.facilityCode && event.latitude == null && !event.facilityAddress) {\n const provider = event.facilityCodeListProvider ?? 'SMDG'\n if (!facilitiesToEnrich.has(event.facilityCode)) {\n facilitiesToEnrich.set(event.facilityCode, {\n code: event.facilityCode,\n provider,\n unlocode: event.locationUnlocode ?? '',\n })\n }\n }\n }\n\n if (facilitiesToEnrich.size === 0) {\n return\n }\n\n // Create BIC API client\n const client = new BicApiClient({\n baseUrl: bicConfig.baseUrl,\n username: bicConfig.username,\n password: bicConfig.password,\n })\n\n // Fetch facility data (one call per unique facility)\n const facilityMap = new Map<string, BicFacility>()\n for (const [code, info] of facilitiesToEnrich) {\n try {\n const facility = await client.getFacility(info.code, info.provider, info.unlocode)\n if (facility) {\n facilityMap.set(code, facility)\n }\n } catch (error) {\n trackingLogger.error('Failed to fetch BIC facility', {\n facilityCode: code,\n codeProvider: info.provider,\n unlocode: info.unlocode,\n shipmentId: shipment.id,\n error: error instanceof Error ? error.message : String(error),\n })\n // Continue with other facilities - don't fail the whole enrichment\n }\n }\n\n if (facilityMap.size === 0) {\n return\n }\n\n // Enrich origin location\n if (shipment.originLocation?.facilityCode && facilityMap.has(shipment.originLocation.facilityCode)) {\n const bic = facilityMap.get(shipment.originLocation.facilityCode)!\n shipment.originLocation = this.mergeLocationWithBicFacility(shipment.originLocation, bic)\n }\n\n // Enrich destination location\n if (shipment.destinationLocation?.facilityCode && facilityMap.has(shipment.destinationLocation.facilityCode)) {\n const bic = facilityMap.get(shipment.destinationLocation.facilityCode)!\n shipment.destinationLocation = this.mergeLocationWithBicFacility(shipment.destinationLocation, bic)\n }\n\n // Enrich route stops\n if (shipment.routeStops) {\n for (const stop of shipment.routeStops) {\n if (stop.facilityCode && facilityMap.has(stop.facilityCode)) {\n const bic = facilityMap.get(stop.facilityCode)!\n const coords = BicApiClient.parseCoordinates(bic)\n const address = BicApiClient.formatAddress(bic)\n \n if (!stop.coords && coords) {\n stop.coords = coords\n }\n if (!stop.facilityAddress && address) {\n stop.facilityAddress = address\n }\n }\n }\n }\n\n // Enrich cargo events\n if (shipment.cargoEvents) {\n for (const event of shipment.cargoEvents) {\n if (event.facilityCode && facilityMap.has(event.facilityCode)) {\n const bic = facilityMap.get(event.facilityCode)!\n const coords = BicApiClient.parseCoordinates(bic)\n const address = BicApiClient.formatAddress(bic)\n \n if (event.latitude == null && coords) {\n event.latitude = coords.latitude\n event.longitude = coords.longitude\n }\n if (!event.facilityAddress && address) {\n event.facilityAddress = address\n }\n }\n }\n }\n }\n\n /**\n * Merges BIC facility data into a FacilityLocation (only fills missing fields).\n */\n private mergeLocationWithBicFacility(location: FacilityLocation, bic: BicFacility): FacilityLocation {\n const coords = BicApiClient.parseCoordinates(bic)\n const address = BicApiClient.formatAddress(bic)\n const operatorName = BicApiClient.getOperatorName(bic)\n const facilityName = BicApiClient.getFacilityName(bic)\n\n return mergeLocationWithBicData(location, {\n name: facilityName ?? undefined,\n address: address ?? undefined,\n coords: coords ?? undefined,\n operatorName: operatorName ?? undefined,\n })\n }\n\n /**\n * Stops an active tracking job (sets status to 'deactivated').\n * Tenant-scoped: refuses to stop a job not belonging to the caller's tenant.\n * Idempotent: returns { stopped: false } when the job is already inactive,\n * doesn't exist, or belongs to a different tenant.\n */\n async stopTrackingJob(\n jobId: string,\n ctx: { organizationId: string; tenantId: string },\n ): Promise<{ stopped: boolean }> {\n const em = this.deps.em()\n const job = await em.findOne(TrackingJob, {\n id: jobId,\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n })\n\n if (!job) {\n return { stopped: false }\n }\n\n if (job.status !== 'active' && job.status !== 'paused') {\n return { stopped: false }\n }\n\n await this.stopJobInline(em, job)\n\n trackingLogger.info('Tracking job stopped', {\n trackingJobId: jobId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n\n return { stopped: true }\n }\n\n /**\n * Stop a job (target status defaults to `deactivated`) and emit\n * `tracking_job.completed`, operating on the given EntityManager (the job must\n * be managed by it). Shared by the public `stopTrackingJob` (externally\n * stopped \u2192 `deactivated`) and the carrier-poll auto-stop (cargo finished \u2192\n * `completed`) so both routes flush + emit identically. Either status is\n * excluded from the `status: 'active'` poll filter, so polling stops.\n */\n private async stopJobInline(\n em: EntityManager,\n job: TrackingJob,\n status: TrackingJobStatusEnum = 'deactivated',\n ): Promise<void> {\n job.status = status\n job.nextPollAt = null\n await em.flush()\n\n await this.deps.eventBus.emit('shipment_tracking.tracking_job.completed', {\n id: job.id,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n })\n }\n\n /**\n * Reconciliation: stops any active TrackingJob that has no folder_legs row\n * referencing it via tracking_job_id. Used by the daily reconciliation cron\n * to clean up orphans created by failed Phase B/C re-tracking flows.\n *\n * Returns { stoppedCount } so the cron can surface to ops.\n * The optional folderLegsTracker param exists so this code can run from\n * @freighttech/shipment-tracking without a hard import on @freighttech/projects:\n * the caller passes a function that returns the set of tracking_job_ids\n * currently referenced by folder_legs.\n */\n async reconcileOrphanedTrackingJobs(\n folderLegTrackingJobIds: Set<string>,\n ctx?: { organizationId?: string; tenantId?: string },\n ): Promise<{ stoppedCount: number; checked: number }> {\n const em = this.deps.em()\n const filter: Record<string, unknown> = {\n status: 'active',\n deletedAt: null,\n }\n if (ctx?.organizationId) filter.organizationId = ctx.organizationId\n if (ctx?.tenantId) filter.tenantId = ctx.tenantId\n\n const activeJobs = await em.find(TrackingJob, filter)\n\n let stoppedCount = 0\n for (const job of activeJobs) {\n if (folderLegTrackingJobIds.has(job.id)) continue\n const result = await this.stopTrackingJob(job.id, {\n organizationId: job.organizationId,\n tenantId: job.tenantId,\n })\n if (result.stopped) stoppedCount++\n }\n\n return { stoppedCount, checked: activeJobs.length }\n }\n\n /**\n * Records an error on a tracking job.\n */\n /**\n * Backs a job off by `backoffMs` and logs `message` in its error history,\n * WITHOUT touching retryCount. For account-level/transient ShipsGo conditions\n * (402 credits, 429 rate limit, 403 auth) that must not consume the per-job\n * failure budget (which would flip the job to 'failed') nor keep re-hitting an\n * exhausted/throttled account every poll. The job resumes on its own.\n */\n private async recordTransientBackoff(\n em: EntityManager,\n job: TrackingJob,\n message: string,\n backoffMs: number,\n ): Promise<void> {\n const history = job.errorHistory ?? []\n history.push({ date: new Date().toISOString(), message })\n if (history.length > 20) history.splice(0, history.length - 20)\n job.errorHistory = history\n job.lastPollAt = new Date()\n job.nextPollAt = new Date(Date.now() + backoffMs)\n await em.flush()\n }\n\n /** ShipsGo credit-exhaustion (HTTP 402): back off ~6h until credits are topped up. */\n private async recordCreditExhaustion(em: EntityManager, job: TrackingJob): Promise<void> {\n await this.recordTransientBackoff(em, job, 'ShipsGo credits exhausted (HTTP 402)', 6 * 60 * 60 * 1000)\n }\n\n private async recordJobError(\n em: EntityManager,\n job: TrackingJob,\n message: string,\n ): Promise<void> {\n const history = job.errorHistory ?? []\n history.push({ date: new Date().toISOString(), message })\n\n // Keep last 20 errors\n if (history.length > 20) {\n history.splice(0, history.length - 20)\n }\n\n job.errorHistory = history\n job.retryCount = (job.retryCount || 0) + 1\n job.lastPollAt = new Date()\n\n // Mark as failed after 10 consecutive errors\n if (job.retryCount >= 10) {\n job.status = 'failed'\n await em.flush()\n\n await this.deps.eventBus.emit('shipment_tracking.tracking_job.failed', {\n id: job.id,\n carrierCode: job.carrierCode,\n retryCount: job.retryCount,\n lastError: message,\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n })\n return\n }\n\n await em.flush()\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["pollResult"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
|
+
import { Shipment, TrackingEvent, Webhook, WebhookDelivery } from "../data/entities.js";
|
|
3
|
+
import { dispatchWebhook } from "../lib/webhook-dispatcher.js";
|
|
4
|
+
import { sleep } from "../lib/background.js";
|
|
5
|
+
const RETRY_DELAYS_MS = [5e3, 3e4, 12e4];
|
|
6
|
+
const MAX_RETRIES = 3;
|
|
7
|
+
function buildShipmentPayload(shipment, trackingEvents) {
|
|
8
|
+
const latestEvent = trackingEvents.length > 0 ? trackingEvents[trackingEvents.length - 1] : null;
|
|
9
|
+
return {
|
|
10
|
+
id: shipment.id,
|
|
11
|
+
status: shipment.status,
|
|
12
|
+
carrierCode: shipment.carrierCode,
|
|
13
|
+
containerNumber: shipment.containerNumber,
|
|
14
|
+
isoEquipmentCode: shipment.isoEquipmentCode,
|
|
15
|
+
bookingNumber: shipment.bookingNumber,
|
|
16
|
+
bolNumber: shipment.bolNumber,
|
|
17
|
+
// Multi-source timestamp arrays (consumers compute primary via "latest updatedAt wins")
|
|
18
|
+
etdTimestamps: shipment.etdTimestamps,
|
|
19
|
+
etaTimestamps: shipment.etaTimestamps,
|
|
20
|
+
atdTimestamps: shipment.atdTimestamps,
|
|
21
|
+
ataTimestamps: shipment.ataTimestamps,
|
|
22
|
+
// Origin/destination (rich JSONB location data)
|
|
23
|
+
originLocation: shipment.originLocation,
|
|
24
|
+
destinationLocation: shipment.destinationLocation,
|
|
25
|
+
// Current location derived from latest event
|
|
26
|
+
currentLocationName: latestEvent?.locationName ?? null,
|
|
27
|
+
currentLocationUnlocode: latestEvent?.locationUnlocode ?? null,
|
|
28
|
+
// Vessel info
|
|
29
|
+
vesselName: shipment.vesselName,
|
|
30
|
+
vesselImo: shipment.vesselImo,
|
|
31
|
+
voyageNumber: shipment.voyageNumber,
|
|
32
|
+
eventCount: shipment.eventCount,
|
|
33
|
+
lastEventAt: shipment.lastEventAt?.toISOString() ?? null,
|
|
34
|
+
extra: shipment.extra,
|
|
35
|
+
createdAt: shipment.createdAt?.toISOString() ?? null,
|
|
36
|
+
updatedAt: shipment.updatedAt?.toISOString() ?? null,
|
|
37
|
+
trackingEvents: trackingEvents.map((event) => ({
|
|
38
|
+
// Event source
|
|
39
|
+
id: event.id,
|
|
40
|
+
source: event.source,
|
|
41
|
+
sourceEventId: event.sourceEventId,
|
|
42
|
+
// Core event fields
|
|
43
|
+
eventType: event.eventType,
|
|
44
|
+
eventCode: event.eventCode,
|
|
45
|
+
eventClassifierCode: event.eventClassifierCode,
|
|
46
|
+
eventDateTime: event.eventDateTime?.toISOString() ?? null,
|
|
47
|
+
eventDateTimeOffset: event.eventDateTimeOffset,
|
|
48
|
+
description: event.description,
|
|
49
|
+
// Equipment fields (DCSA EQUIPMENT events)
|
|
50
|
+
equipmentReference: event.equipmentReference,
|
|
51
|
+
isoEquipmentCode: event.isoEquipmentCode,
|
|
52
|
+
emptyIndicatorCode: event.emptyIndicatorCode,
|
|
53
|
+
isTransshipmentMove: event.isTransshipmentMove,
|
|
54
|
+
// Location fields
|
|
55
|
+
locationName: event.locationName,
|
|
56
|
+
locationUnlocode: event.locationUnlocode,
|
|
57
|
+
locationCountry: event.locationCountry,
|
|
58
|
+
facilityCode: event.facilityCode,
|
|
59
|
+
facilityCodeListProvider: event.facilityCodeListProvider,
|
|
60
|
+
facilityTypeCode: event.facilityTypeCode,
|
|
61
|
+
latitude: event.latitude,
|
|
62
|
+
longitude: event.longitude,
|
|
63
|
+
// Transport call fields
|
|
64
|
+
transportCallReference: event.transportCallReference,
|
|
65
|
+
modeOfTransport: event.modeOfTransport,
|
|
66
|
+
vesselName: event.vesselName,
|
|
67
|
+
vesselImo: event.vesselImo,
|
|
68
|
+
voyageNumber: event.voyageNumber,
|
|
69
|
+
carrierServiceCode: event.carrierServiceCode,
|
|
70
|
+
carrierExportVoyageNumber: event.carrierExportVoyageNumber,
|
|
71
|
+
carrierImportVoyageNumber: event.carrierImportVoyageNumber,
|
|
72
|
+
universalServiceReference: event.universalServiceReference,
|
|
73
|
+
universalExportVoyageReference: event.universalExportVoyageReference,
|
|
74
|
+
universalImportVoyageReference: event.universalImportVoyageReference,
|
|
75
|
+
portVisitReference: event.portVisitReference,
|
|
76
|
+
// Document references
|
|
77
|
+
relatedDocumentReferences: event.relatedDocumentReferences,
|
|
78
|
+
// Metadata fields
|
|
79
|
+
eventCreatedDateTime: event.eventCreatedDateTime?.toISOString() ?? null,
|
|
80
|
+
retractedEventId: event.retractedEventId,
|
|
81
|
+
publisherName: event.publisherName,
|
|
82
|
+
publisherRole: event.publisherRole,
|
|
83
|
+
// Additional fields
|
|
84
|
+
delayReasonCode: event.delayReasonCode,
|
|
85
|
+
changeRemark: event.changeRemark,
|
|
86
|
+
seals: event.seals,
|
|
87
|
+
// Timestamps
|
|
88
|
+
createdAt: event.createdAt?.toISOString() ?? null
|
|
89
|
+
}))
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
class WebhookService {
|
|
93
|
+
constructor(deps) {
|
|
94
|
+
this.deps = deps;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Builds the full shipment payload including tracking events.
|
|
98
|
+
* Events are fetched from the TrackingJob associated with the shipment.
|
|
99
|
+
*/
|
|
100
|
+
async buildFullShipmentPayload(shipmentId) {
|
|
101
|
+
const em = this.deps.em();
|
|
102
|
+
const shipment = await em.findOne(Shipment, { id: shipmentId, deletedAt: null }, { populate: ["trackingJob"] });
|
|
103
|
+
if (!shipment) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
let trackingEvents = [];
|
|
107
|
+
if (shipment.trackingJob) {
|
|
108
|
+
const allEvents = await em.find(
|
|
109
|
+
TrackingEvent,
|
|
110
|
+
{ trackingJob: shipment.trackingJob },
|
|
111
|
+
{ orderBy: { eventDateTime: "asc" } }
|
|
112
|
+
);
|
|
113
|
+
trackingEvents = allEvents.filter((event) => {
|
|
114
|
+
if (event.eventType === "TRANSPORT") return true;
|
|
115
|
+
if (event.eventType === "EQUIPMENT") {
|
|
116
|
+
return event.equipmentReference === shipment.containerNumber;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return buildShipmentPayload(shipment, trackingEvents);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Dispatches webhooks for a shipment event with full data and retry logic.
|
|
125
|
+
*
|
|
126
|
+
* @param input.eventType - The event type (e.g., 'shipment.created', 'shipment.tracking_update')
|
|
127
|
+
* @param input.shipmentPayload - Full shipment data including cargo events
|
|
128
|
+
* @param input.tenantId - Tenant ID for finding matching webhooks
|
|
129
|
+
* @param input.organizationId - Organization ID for finding matching webhooks
|
|
130
|
+
* @returns Number of webhooks successfully dispatched
|
|
131
|
+
*/
|
|
132
|
+
async dispatchWithRetry(input) {
|
|
133
|
+
const em = this.deps.em();
|
|
134
|
+
const webhooks = await findWithDecryption(
|
|
135
|
+
em,
|
|
136
|
+
Webhook,
|
|
137
|
+
{
|
|
138
|
+
tenantId: input.tenantId,
|
|
139
|
+
organizationId: input.organizationId,
|
|
140
|
+
isActive: true
|
|
141
|
+
},
|
|
142
|
+
void 0,
|
|
143
|
+
{ tenantId: input.tenantId, organizationId: input.organizationId }
|
|
144
|
+
);
|
|
145
|
+
const matching = webhooks.filter((webhook) => {
|
|
146
|
+
return webhook.eventsSubscribed.some((pattern) => {
|
|
147
|
+
if (pattern === input.eventType) return true;
|
|
148
|
+
if (pattern === "*") return true;
|
|
149
|
+
if (pattern.endsWith(".*")) {
|
|
150
|
+
const prefix = pattern.slice(0, -2);
|
|
151
|
+
if (input.eventType.startsWith(prefix + ".")) return true;
|
|
152
|
+
}
|
|
153
|
+
if (pattern.endsWith(".*")) {
|
|
154
|
+
const prefix = pattern.slice(0, -1);
|
|
155
|
+
if (input.eventType.startsWith(prefix)) return true;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
if (matching.length === 0) {
|
|
161
|
+
console.debug("[shipment-tracking:webhook] No matching webhooks found for event:", input.eventType, {
|
|
162
|
+
totalWebhooks: webhooks.length,
|
|
163
|
+
webhookPatterns: webhooks.map((w) => w.eventsSubscribed)
|
|
164
|
+
});
|
|
165
|
+
return { success: true, dispatched: 0, failed: 0 };
|
|
166
|
+
}
|
|
167
|
+
console.log("[shipment-tracking:webhook] Dispatching to", matching.length, "webhooks for event:", input.eventType);
|
|
168
|
+
let dispatched = 0;
|
|
169
|
+
let failed = 0;
|
|
170
|
+
const fullPayload = {
|
|
171
|
+
type: input.eventType,
|
|
172
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
173
|
+
...input.shipmentPayload
|
|
174
|
+
};
|
|
175
|
+
for (const webhook of matching) {
|
|
176
|
+
const result = await this.dispatchSingleWebhook(em, webhook, fullPayload, input.eventType);
|
|
177
|
+
if (result.success) {
|
|
178
|
+
dispatched++;
|
|
179
|
+
} else {
|
|
180
|
+
failed++;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
success: failed === 0,
|
|
185
|
+
dispatched,
|
|
186
|
+
failed
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Dispatches a single webhook with retry logic.
|
|
191
|
+
* Records delivery attempts in the WebhookDelivery table for audit.
|
|
192
|
+
*/
|
|
193
|
+
async dispatchSingleWebhook(em, webhook, payload, eventType) {
|
|
194
|
+
const delivery = em.create(WebhookDelivery, {
|
|
195
|
+
webhook: em.getReference(Webhook, webhook.id),
|
|
196
|
+
eventType,
|
|
197
|
+
status: "pending",
|
|
198
|
+
payload,
|
|
199
|
+
retryCount: 0
|
|
200
|
+
});
|
|
201
|
+
em.persist(delivery);
|
|
202
|
+
await em.flush();
|
|
203
|
+
let lastResult = null;
|
|
204
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
205
|
+
lastResult = await dispatchWebhook({
|
|
206
|
+
url: webhook.url,
|
|
207
|
+
payload,
|
|
208
|
+
hmacSecret: webhook.hmacSecret
|
|
209
|
+
});
|
|
210
|
+
if (lastResult.success) {
|
|
211
|
+
delivery.status = "success";
|
|
212
|
+
delivery.responseStatus = lastResult.responseStatus ?? null;
|
|
213
|
+
delivery.responseBody = lastResult.responseBody ?? null;
|
|
214
|
+
delivery.retryCount = attempt;
|
|
215
|
+
await em.flush();
|
|
216
|
+
await this.deps.eventBus.emit("shipment_tracking.webhook.delivery_success", {
|
|
217
|
+
deliveryId: delivery.id,
|
|
218
|
+
webhookId: webhook.id,
|
|
219
|
+
eventType,
|
|
220
|
+
responseStatus: lastResult.responseStatus
|
|
221
|
+
});
|
|
222
|
+
return { success: true };
|
|
223
|
+
}
|
|
224
|
+
delivery.responseStatus = lastResult.responseStatus ?? null;
|
|
225
|
+
delivery.responseBody = lastResult.responseBody ?? null;
|
|
226
|
+
delivery.errorMessage = lastResult.errorMessage ?? null;
|
|
227
|
+
delivery.retryCount = attempt + 1;
|
|
228
|
+
if (attempt < MAX_RETRIES) {
|
|
229
|
+
const delayMs = RETRY_DELAYS_MS[attempt] ?? RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1];
|
|
230
|
+
await sleep(delayMs);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
delivery.status = "failed";
|
|
234
|
+
await em.flush();
|
|
235
|
+
await this.deps.eventBus.emit("shipment_tracking.webhook.delivery_failed", {
|
|
236
|
+
deliveryId: delivery.id,
|
|
237
|
+
webhookId: webhook.id,
|
|
238
|
+
eventType,
|
|
239
|
+
retryCount: delivery.retryCount,
|
|
240
|
+
lastError: lastResult?.errorMessage ?? "Unknown error"
|
|
241
|
+
});
|
|
242
|
+
return { success: false };
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Legacy method for backwards compatibility with existing subscribers.
|
|
246
|
+
* Finds matching webhooks and dispatches them (used by kept subscribers).
|
|
247
|
+
*
|
|
248
|
+
* @deprecated Use dispatchWithRetry() for new code
|
|
249
|
+
*/
|
|
250
|
+
async dispatchEvent(input) {
|
|
251
|
+
const result = await this.dispatchWithRetry({
|
|
252
|
+
eventType: input.eventType,
|
|
253
|
+
shipmentPayload: input.payload,
|
|
254
|
+
tenantId: input.tenantId,
|
|
255
|
+
organizationId: input.organizationId
|
|
256
|
+
});
|
|
257
|
+
return result.dispatched;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
export {
|
|
261
|
+
WebhookService
|
|
262
|
+
};
|
|
263
|
+
//# sourceMappingURL=webhookService.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/shipment_tracking/services/webhookService.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { EventBus } from '@open-mercato/events'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Shipment, TrackingEvent, TrackingJob, Webhook, WebhookDelivery } from '../data/entities'\nimport { dispatchWebhook } from '../lib/webhook-dispatcher'\nimport { sleep } from '../lib/background'\n\ntype WebhookServiceDeps = {\n em: () => EntityManager\n eventBus: EventBus\n}\n\nconst RETRY_DELAYS_MS = [5_000, 30_000, 120_000] // 5s, 30s, 2min\nconst MAX_RETRIES = 3\n\n/**\n * Builds a full shipment payload including tracking events for webhook dispatch.\n * Includes all DCSA T&T v3.0 fields.\n * Timestamps are provided as multi-source arrays; consumers compute primary via \"latest updatedAt wins\".\n */\nfunction buildShipmentPayload(shipment: Shipment, trackingEvents: TrackingEvent[]): Record<string, unknown> {\n // Get current location from latest tracking event (derived at query time)\n const latestEvent = trackingEvents.length > 0 ? trackingEvents[trackingEvents.length - 1] : null\n\n return {\n id: shipment.id,\n status: shipment.status,\n carrierCode: shipment.carrierCode,\n containerNumber: shipment.containerNumber,\n isoEquipmentCode: shipment.isoEquipmentCode,\n bookingNumber: shipment.bookingNumber,\n bolNumber: shipment.bolNumber,\n // Multi-source timestamp arrays (consumers compute primary via \"latest updatedAt wins\")\n etdTimestamps: shipment.etdTimestamps,\n etaTimestamps: shipment.etaTimestamps,\n atdTimestamps: shipment.atdTimestamps,\n ataTimestamps: shipment.ataTimestamps,\n // Origin/destination (rich JSONB location data)\n originLocation: shipment.originLocation,\n destinationLocation: shipment.destinationLocation,\n // Current location derived from latest event\n currentLocationName: latestEvent?.locationName ?? null,\n currentLocationUnlocode: latestEvent?.locationUnlocode ?? null,\n // Vessel info\n vesselName: shipment.vesselName,\n vesselImo: shipment.vesselImo,\n voyageNumber: shipment.voyageNumber,\n eventCount: shipment.eventCount,\n lastEventAt: shipment.lastEventAt?.toISOString() ?? null,\n extra: shipment.extra,\n createdAt: shipment.createdAt?.toISOString() ?? null,\n updatedAt: shipment.updatedAt?.toISOString() ?? null,\n trackingEvents: trackingEvents.map((event) => ({\n // Event source\n id: event.id,\n source: event.source,\n sourceEventId: event.sourceEventId,\n\n // Core event fields\n eventType: event.eventType,\n eventCode: event.eventCode,\n eventClassifierCode: event.eventClassifierCode,\n eventDateTime: event.eventDateTime?.toISOString() ?? null,\n eventDateTimeOffset: event.eventDateTimeOffset,\n description: event.description,\n\n // Equipment fields (DCSA EQUIPMENT events)\n equipmentReference: event.equipmentReference,\n isoEquipmentCode: event.isoEquipmentCode,\n emptyIndicatorCode: event.emptyIndicatorCode,\n isTransshipmentMove: event.isTransshipmentMove,\n\n // Location fields\n locationName: event.locationName,\n locationUnlocode: event.locationUnlocode,\n locationCountry: event.locationCountry,\n facilityCode: event.facilityCode,\n facilityCodeListProvider: event.facilityCodeListProvider,\n facilityTypeCode: event.facilityTypeCode,\n latitude: event.latitude,\n longitude: event.longitude,\n\n // Transport call fields\n transportCallReference: event.transportCallReference,\n modeOfTransport: event.modeOfTransport,\n vesselName: event.vesselName,\n vesselImo: event.vesselImo,\n voyageNumber: event.voyageNumber,\n carrierServiceCode: event.carrierServiceCode,\n carrierExportVoyageNumber: event.carrierExportVoyageNumber,\n carrierImportVoyageNumber: event.carrierImportVoyageNumber,\n universalServiceReference: event.universalServiceReference,\n universalExportVoyageReference: event.universalExportVoyageReference,\n universalImportVoyageReference: event.universalImportVoyageReference,\n portVisitReference: event.portVisitReference,\n\n // Document references\n relatedDocumentReferences: event.relatedDocumentReferences,\n\n // Metadata fields\n eventCreatedDateTime: event.eventCreatedDateTime?.toISOString() ?? null,\n retractedEventId: event.retractedEventId,\n publisherName: event.publisherName,\n publisherRole: event.publisherRole,\n\n // Additional fields\n delayReasonCode: event.delayReasonCode,\n changeRemark: event.changeRemark,\n seals: event.seals,\n\n // Timestamps\n createdAt: event.createdAt?.toISOString() ?? null,\n })),\n }\n}\n\n/**\n * WebhookService handles finding matching webhooks and dispatching them\n * with full shipment data and retry logic.\n */\nexport class WebhookService {\n private deps: WebhookServiceDeps\n\n constructor(deps: WebhookServiceDeps) {\n this.deps = deps\n }\n\n /**\n * Builds the full shipment payload including tracking events.\n * Events are fetched from the TrackingJob associated with the shipment.\n */\n async buildFullShipmentPayload(shipmentId: string): Promise<Record<string, unknown> | null> {\n const em = this.deps.em()\n\n const shipment = await em.findOne(Shipment, { id: shipmentId, deletedAt: null }, { populate: ['trackingJob'] })\n if (!shipment) {\n return null\n }\n\n // If shipment has a tracking job, get events filtered for this container\n let trackingEvents: TrackingEvent[] = []\n if (shipment.trackingJob) {\n const allEvents = await em.find(\n TrackingEvent,\n { trackingJob: shipment.trackingJob },\n { orderBy: { eventDateTime: 'asc' } },\n )\n // Filter events for this specific container\n // Include TRANSPORT events (no equipmentReference) and EQUIPMENT events matching this container\n trackingEvents = allEvents.filter((event) => {\n if (event.eventType === 'TRANSPORT') return true\n if (event.eventType === 'EQUIPMENT') {\n return event.equipmentReference === shipment.containerNumber\n }\n return false\n })\n }\n\n return buildShipmentPayload(shipment, trackingEvents)\n }\n\n /**\n * Dispatches webhooks for a shipment event with full data and retry logic.\n *\n * @param input.eventType - The event type (e.g., 'shipment.created', 'shipment.tracking_update')\n * @param input.shipmentPayload - Full shipment data including cargo events\n * @param input.tenantId - Tenant ID for finding matching webhooks\n * @param input.organizationId - Organization ID for finding matching webhooks\n * @returns Number of webhooks successfully dispatched\n */\n async dispatchWithRetry(input: {\n eventType: string\n shipmentPayload: Record<string, unknown>\n tenantId: string\n organizationId: string\n }): Promise<{ success: boolean; dispatched: number; failed: number }> {\n const em = this.deps.em()\n\n // Find matching webhooks\n const webhooks = await findWithDecryption(\n em,\n Webhook,\n {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n isActive: true,\n },\n undefined,\n { tenantId: input.tenantId, organizationId: input.organizationId },\n )\n\n // Filter to webhooks subscribed to this event type\n const matching = webhooks.filter((webhook) => {\n return webhook.eventsSubscribed.some((pattern) => {\n // Exact match\n if (pattern === input.eventType) return true\n // Global wildcard\n if (pattern === '*') return true\n // Module wildcard (e.g., 'shipment_tracking.*' matches 'shipment_tracking.shipment.created')\n if (pattern.endsWith('.*')) {\n const prefix = pattern.slice(0, -2)\n if (input.eventType.startsWith(prefix + '.')) return true\n }\n // Entity wildcard (e.g., 'shipment_tracking.shipment.*' matches 'shipment_tracking.shipment.created')\n if (pattern.endsWith('.*')) {\n const prefix = pattern.slice(0, -1)\n if (input.eventType.startsWith(prefix)) return true\n }\n return false\n })\n })\n\n if (matching.length === 0) {\n console.debug('[shipment-tracking:webhook] No matching webhooks found for event:', input.eventType, {\n totalWebhooks: webhooks.length,\n webhookPatterns: webhooks.map((w) => w.eventsSubscribed),\n })\n return { success: true, dispatched: 0, failed: 0 }\n }\n\n console.log('[shipment-tracking:webhook] Dispatching to', matching.length, 'webhooks for event:', input.eventType)\n\n let dispatched = 0\n let failed = 0\n\n const fullPayload = {\n type: input.eventType,\n timestamp: new Date().toISOString(),\n ...input.shipmentPayload,\n }\n\n for (const webhook of matching) {\n const result = await this.dispatchSingleWebhook(em, webhook, fullPayload, input.eventType)\n if (result.success) {\n dispatched++\n } else {\n failed++\n }\n }\n\n return {\n success: failed === 0,\n dispatched,\n failed,\n }\n }\n\n /**\n * Dispatches a single webhook with retry logic.\n * Records delivery attempts in the WebhookDelivery table for audit.\n */\n private async dispatchSingleWebhook(\n em: EntityManager,\n webhook: Webhook,\n payload: Record<string, unknown>,\n eventType: string,\n ): Promise<{ success: boolean }> {\n // Create delivery record for audit\n // Use getReference to create a managed reference since webhook may be detached after decryption\n const delivery = em.create(WebhookDelivery, {\n webhook: em.getReference(Webhook, webhook.id),\n eventType,\n status: 'pending',\n payload,\n retryCount: 0,\n })\n em.persist(delivery)\n await em.flush()\n\n let lastResult: Awaited<ReturnType<typeof dispatchWebhook>> | null = null\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n lastResult = await dispatchWebhook({\n url: webhook.url,\n payload,\n hmacSecret: webhook.hmacSecret,\n })\n\n if (lastResult.success) {\n // Success - update delivery record\n delivery.status = 'success'\n delivery.responseStatus = lastResult.responseStatus ?? null\n delivery.responseBody = lastResult.responseBody ?? null\n delivery.retryCount = attempt\n await em.flush()\n\n // Emit success event for monitoring\n await this.deps.eventBus.emit('shipment_tracking.webhook.delivery_success', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType,\n responseStatus: lastResult.responseStatus,\n })\n\n return { success: true }\n }\n\n // Failed - update delivery record with error\n delivery.responseStatus = lastResult.responseStatus ?? null\n delivery.responseBody = lastResult.responseBody ?? null\n delivery.errorMessage = lastResult.errorMessage ?? null\n delivery.retryCount = attempt + 1\n\n if (attempt < MAX_RETRIES) {\n // Wait before retry\n const delayMs = RETRY_DELAYS_MS[attempt] ?? RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1]\n await sleep(delayMs)\n }\n }\n\n // All retries exhausted\n delivery.status = 'failed'\n await em.flush()\n\n // Emit failure event for monitoring\n await this.deps.eventBus.emit('shipment_tracking.webhook.delivery_failed', {\n deliveryId: delivery.id,\n webhookId: webhook.id,\n eventType,\n retryCount: delivery.retryCount,\n lastError: lastResult?.errorMessage ?? 'Unknown error',\n })\n\n return { success: false }\n }\n\n /**\n * Legacy method for backwards compatibility with existing subscribers.\n * Finds matching webhooks and dispatches them (used by kept subscribers).\n *\n * @deprecated Use dispatchWithRetry() for new code\n */\n async dispatchEvent(input: {\n eventType: string\n payload: Record<string, unknown>\n tenantId: string\n organizationId: string\n }): Promise<number> {\n const result = await this.dispatchWithRetry({\n eventType: input.eventType,\n shipmentPayload: input.payload,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n })\n return result.dispatched\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const setup = {
|
|
2
|
+
defaultRoleFeatures: {
|
|
3
|
+
admin: ["shipment_tracking.*"],
|
|
4
|
+
employee: [
|
|
5
|
+
"shipment_tracking.shipments.view",
|
|
6
|
+
"shipment_tracking.tracking_jobs.view",
|
|
7
|
+
"shipment_tracking.carrier_configs.view",
|
|
8
|
+
"shipment_tracking.shipsgo_config.view",
|
|
9
|
+
"shipment_tracking.webhooks.view",
|
|
10
|
+
"shipment_tracking.location_overrides.view"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
seedDefaults: async (ctx) => {
|
|
14
|
+
try {
|
|
15
|
+
const schedulerService = ctx.container.resolve("schedulerService");
|
|
16
|
+
await schedulerService.register({
|
|
17
|
+
name: "Daily Shipment Tracking Poll",
|
|
18
|
+
description: "Polls all active tracking jobs for carrier updates every day at 6:00 AM UTC. You can change the schedule time in the scheduler settings.",
|
|
19
|
+
scopeType: "organization",
|
|
20
|
+
tenantId: ctx.tenantId,
|
|
21
|
+
organizationId: ctx.organizationId,
|
|
22
|
+
scheduleType: "cron",
|
|
23
|
+
scheduleValue: "0 6 * * *",
|
|
24
|
+
// 6:00 AM UTC daily
|
|
25
|
+
timezone: "UTC",
|
|
26
|
+
targetType: "command",
|
|
27
|
+
targetCommand: "shipment_tracking.tracking.poll_all",
|
|
28
|
+
targetPayload: {
|
|
29
|
+
tenantId: ctx.tenantId,
|
|
30
|
+
organizationId: ctx.organizationId
|
|
31
|
+
},
|
|
32
|
+
sourceType: "module",
|
|
33
|
+
sourceModule: "shipment_tracking",
|
|
34
|
+
isEnabled: true
|
|
35
|
+
});
|
|
36
|
+
console.log(
|
|
37
|
+
`[shipment-tracking] Registered daily poll schedule for org ${ctx.organizationId}`
|
|
38
|
+
);
|
|
39
|
+
await schedulerService.register({
|
|
40
|
+
name: "Pre-Arrival Status Evaluation",
|
|
41
|
+
description: "Checks IN_TRANSIT shipments every 6 hours and upgrades them to PRE_ARRIVAL when ETA is within 7 days.",
|
|
42
|
+
scopeType: "organization",
|
|
43
|
+
tenantId: ctx.tenantId,
|
|
44
|
+
organizationId: ctx.organizationId,
|
|
45
|
+
scheduleType: "cron",
|
|
46
|
+
scheduleValue: "0 */6 * * *",
|
|
47
|
+
// Every 6 hours
|
|
48
|
+
timezone: "UTC",
|
|
49
|
+
targetType: "command",
|
|
50
|
+
targetCommand: "shipment_tracking.tracking.evaluate_pre_arrival",
|
|
51
|
+
targetPayload: {
|
|
52
|
+
tenantId: ctx.tenantId,
|
|
53
|
+
organizationId: ctx.organizationId
|
|
54
|
+
},
|
|
55
|
+
sourceType: "module",
|
|
56
|
+
sourceModule: "shipment_tracking",
|
|
57
|
+
isEnabled: true
|
|
58
|
+
});
|
|
59
|
+
console.log(
|
|
60
|
+
`[shipment-tracking] Registered pre-arrival evaluation schedule for org ${ctx.organizationId}`
|
|
61
|
+
);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.debug("[shipment-tracking] Scheduler service not available, skipping schedule registration");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var setup_default = setup;
|
|
68
|
+
export {
|
|
69
|
+
setup_default as default,
|
|
70
|
+
setup
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=setup.js.map
|