@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,1283 @@
|
|
|
1
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
|
+
import { Shipment, TrackingJob, TrackingEvent, CarrierConfig, BicConfig } from "../data/entities.js";
|
|
3
|
+
import { ShipsGoCreditsExhaustedError, ShipsGoAuthError, ShipsGoRateLimitedError } from "../lib/shipsgo-client.js";
|
|
4
|
+
import { checkRateLimit } from "../lib/rate-limiter.js";
|
|
5
|
+
import { trackingLogger } from "../lib/logger.js";
|
|
6
|
+
import { deriveShipmentStatus, deriveAirShipmentStatus } from "../lib/status-machine.js";
|
|
7
|
+
import { extractShipmentTimes } from "../lib/time-extraction.js";
|
|
8
|
+
import { generatePollSchedule, getNextPollDate } from "../lib/schedule-generator.js";
|
|
9
|
+
import { mapDcsaEventToWebhookType, isSignificantMilestone } from "../lib/dcsa-event-mapping.js";
|
|
10
|
+
import { inferRouteFromEvents } from "../lib/route-inference.js";
|
|
11
|
+
import { allContainersComplete, destinationArrivedBefore, staleArrivalCutoff } from "../lib/empty-return.js";
|
|
12
|
+
import { mergeExtractedTimestamps, getPrimaryTimestampValue } from "../lib/timestamp-utils.js";
|
|
13
|
+
import { findLatestVesselInfo } from "../lib/vessel-extraction.js";
|
|
14
|
+
import { extractRouteFromEvents, mapTrackingEventToEntry } from "../lib/route-extraction.js";
|
|
15
|
+
import { buildLocationFromEvent, createBasicLocation, mergeLocationWithBicData, isLocationComplete } from "../lib/location-types.js";
|
|
16
|
+
import { BicApiClient } from "../lib/bic-api-client.js";
|
|
17
|
+
import { applyLocationOverrideIfExists } from "../lib/location-overrides.js";
|
|
18
|
+
class TrackingService {
|
|
19
|
+
constructor(deps) {
|
|
20
|
+
this.deps = deps;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates a tracking job and immediately polls the carrier.
|
|
24
|
+
* This is the main entry point for new tracking requests.
|
|
25
|
+
*
|
|
26
|
+
* The system will:
|
|
27
|
+
* 1. Create a TrackingJob for the given carrier and reference
|
|
28
|
+
* 2. Poll the carrier API for events
|
|
29
|
+
* 3. Auto-discover containers from EQUIPMENT events and create Shipments
|
|
30
|
+
* 4. Auto-infer origin/destination ports from events if not provided
|
|
31
|
+
* 5. Emit events for new cargo events and shipment status changes
|
|
32
|
+
*/
|
|
33
|
+
async createTrackingJob(input) {
|
|
34
|
+
const em = this.deps.em();
|
|
35
|
+
const { organizationId, tenantId, carrierCode, referenceType, referenceValue, schedule } = input;
|
|
36
|
+
const provider = input.provider ?? "carrier";
|
|
37
|
+
const mode = input.mode ?? "ocean";
|
|
38
|
+
let originUnlocode = input.originUnlocode?.toUpperCase();
|
|
39
|
+
let destinationUnlocode = input.destinationUnlocode?.toUpperCase();
|
|
40
|
+
const carrierCodeLower = carrierCode.toLowerCase();
|
|
41
|
+
if (provider === "carrier" && !this.deps.carrierRegistry.has(carrierCodeLower)) {
|
|
42
|
+
throw new Error(`No adapter registered for carrier: ${carrierCode}`);
|
|
43
|
+
}
|
|
44
|
+
const existingJob = await em.findOne(TrackingJob, {
|
|
45
|
+
organizationId,
|
|
46
|
+
tenantId,
|
|
47
|
+
carrierCode: carrierCodeLower,
|
|
48
|
+
referenceType,
|
|
49
|
+
referenceValue,
|
|
50
|
+
status: { $in: ["active", "paused"] },
|
|
51
|
+
deletedAt: null
|
|
52
|
+
});
|
|
53
|
+
if (existingJob) {
|
|
54
|
+
trackingLogger.debug("TrackingJob already exists", {
|
|
55
|
+
trackingJobId: existingJob.id,
|
|
56
|
+
carrierCode: carrierCodeLower,
|
|
57
|
+
referenceType,
|
|
58
|
+
referenceValue
|
|
59
|
+
});
|
|
60
|
+
const pollResult2 = await this.pollTrackingJob(existingJob.id);
|
|
61
|
+
return {
|
|
62
|
+
trackingJobId: existingJob.id,
|
|
63
|
+
shipmentsCreated: pollResult2.shipmentsCreated,
|
|
64
|
+
newEvents: pollResult2.newEvents
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const pollSchedule = schedule ?? generatePollSchedule({});
|
|
68
|
+
const nextPollAt = getNextPollDate(pollSchedule);
|
|
69
|
+
const trackingJob = em.create(TrackingJob, {
|
|
70
|
+
organizationId,
|
|
71
|
+
tenantId,
|
|
72
|
+
provider,
|
|
73
|
+
mode,
|
|
74
|
+
carrierCode: carrierCodeLower,
|
|
75
|
+
referenceType,
|
|
76
|
+
referenceValue,
|
|
77
|
+
originUnlocode: originUnlocode ?? null,
|
|
78
|
+
destinationUnlocode: destinationUnlocode ?? null,
|
|
79
|
+
status: "active",
|
|
80
|
+
schedule: pollSchedule,
|
|
81
|
+
nextPollAt
|
|
82
|
+
});
|
|
83
|
+
em.persist(trackingJob);
|
|
84
|
+
await em.flush();
|
|
85
|
+
trackingLogger.info("Created TrackingJob", {
|
|
86
|
+
trackingJobId: trackingJob.id,
|
|
87
|
+
carrierCode: carrierCodeLower,
|
|
88
|
+
referenceType,
|
|
89
|
+
referenceValue,
|
|
90
|
+
tenantId,
|
|
91
|
+
organizationId
|
|
92
|
+
});
|
|
93
|
+
await this.deps.eventBus.emit("shipment_tracking.tracking_job.created", {
|
|
94
|
+
id: trackingJob.id,
|
|
95
|
+
carrierCode: carrierCodeLower,
|
|
96
|
+
referenceType,
|
|
97
|
+
referenceValue,
|
|
98
|
+
tenantId,
|
|
99
|
+
organizationId
|
|
100
|
+
});
|
|
101
|
+
let pollResult = { shipmentsCreated: 0, newEvents: 0 };
|
|
102
|
+
try {
|
|
103
|
+
pollResult = await this.pollTrackingJob(trackingJob.id);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
trackingLogger.error("Initial poll failed", {
|
|
106
|
+
trackingJobId: trackingJob.id,
|
|
107
|
+
carrierCode: carrierCodeLower,
|
|
108
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
109
|
+
});
|
|
110
|
+
await this.deps.eventBus.emit("shipment_tracking.tracking_job.poll_failed", {
|
|
111
|
+
id: trackingJob.id,
|
|
112
|
+
carrierCode: carrierCodeLower,
|
|
113
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
114
|
+
tenantId,
|
|
115
|
+
organizationId
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
trackingJobId: trackingJob.id,
|
|
120
|
+
shipmentsCreated: pollResult.shipmentsCreated,
|
|
121
|
+
newEvents: pollResult.newEvents
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Polls all active tracking jobs for a tenant/organization.
|
|
126
|
+
* Called by the scheduler for periodic re-polling.
|
|
127
|
+
*/
|
|
128
|
+
async pollAllActiveJobs(tenantId, organizationId) {
|
|
129
|
+
const em = this.deps.em();
|
|
130
|
+
const filter = {
|
|
131
|
+
tenantId,
|
|
132
|
+
status: "active",
|
|
133
|
+
deletedAt: null
|
|
134
|
+
};
|
|
135
|
+
if (organizationId) {
|
|
136
|
+
filter.organizationId = organizationId;
|
|
137
|
+
}
|
|
138
|
+
const jobs = await em.find(TrackingJob, filter);
|
|
139
|
+
let polled = 0;
|
|
140
|
+
let totalNewEvents = 0;
|
|
141
|
+
let failed = 0;
|
|
142
|
+
for (const job of jobs) {
|
|
143
|
+
try {
|
|
144
|
+
const result = await this.pollTrackingJob(job.id);
|
|
145
|
+
polled++;
|
|
146
|
+
totalNewEvents += result.newEvents;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
failed++;
|
|
149
|
+
trackingLogger.error("Poll failed for job", {
|
|
150
|
+
trackingJobId: job.id,
|
|
151
|
+
carrierCode: job.carrierCode,
|
|
152
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
153
|
+
});
|
|
154
|
+
await this.deps.eventBus.emit("shipment_tracking.tracking_job.poll_failed", {
|
|
155
|
+
id: job.id,
|
|
156
|
+
carrierCode: job.carrierCode,
|
|
157
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
158
|
+
tenantId: job.tenantId,
|
|
159
|
+
organizationId: job.organizationId
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
trackingLogger.info("Poll all completed", {
|
|
164
|
+
polled,
|
|
165
|
+
totalNewEvents,
|
|
166
|
+
failed,
|
|
167
|
+
tenantId,
|
|
168
|
+
organizationId
|
|
169
|
+
});
|
|
170
|
+
return { polled, newEvents: totalNewEvents, failed };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Polls a single tracking job:
|
|
174
|
+
* 1. Fetches events from the carrier
|
|
175
|
+
* 2. Persists new TrackingEvents
|
|
176
|
+
* 3. Auto-discovers containers and creates/updates Shipments
|
|
177
|
+
* 4. Emits events for cargo events and shipment changes
|
|
178
|
+
*/
|
|
179
|
+
async pollTrackingJob(jobId) {
|
|
180
|
+
const em = this.deps.em();
|
|
181
|
+
const job = await em.findOne(TrackingJob, { id: jobId }, { populate: ["shipments"] });
|
|
182
|
+
if (!job) {
|
|
183
|
+
throw new Error(`TrackingJob not found: ${jobId}`);
|
|
184
|
+
}
|
|
185
|
+
if (job.status !== "active") {
|
|
186
|
+
return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 };
|
|
187
|
+
}
|
|
188
|
+
const scope = { tenantId: job.tenantId, organizationId: job.organizationId };
|
|
189
|
+
let fetchedEvents;
|
|
190
|
+
let carrierResult;
|
|
191
|
+
if (job.provider === "shipsgo") {
|
|
192
|
+
const pollContext = await this.deps.shipsGoProvider.resolvePollContext(scope, job.mode);
|
|
193
|
+
const limitResult = await checkRateLimit(
|
|
194
|
+
this.deps.cacheService,
|
|
195
|
+
pollContext.rateLimitIdentity,
|
|
196
|
+
"shipsgo",
|
|
197
|
+
pollContext.rateLimit.requests,
|
|
198
|
+
pollContext.rateLimit.windowSeconds
|
|
199
|
+
);
|
|
200
|
+
if (!limitResult.allowed) {
|
|
201
|
+
trackingLogger.debug("ShipsGo rate limited, skipping poll", {
|
|
202
|
+
trackingJobId: jobId,
|
|
203
|
+
retryAfterSeconds: limitResult.retryAfterSeconds
|
|
204
|
+
});
|
|
205
|
+
return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 };
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const result = await this.deps.shipsGoProvider.fetchOrRegister(job, pollContext.config);
|
|
209
|
+
fetchedEvents = result.events;
|
|
210
|
+
carrierResult = {
|
|
211
|
+
vesselName: result.vesselName,
|
|
212
|
+
vesselImo: result.vesselImo,
|
|
213
|
+
bookingNumber: result.bookingNumber,
|
|
214
|
+
bolNumber: result.bolNumber,
|
|
215
|
+
awbNumber: result.awbNumber,
|
|
216
|
+
flightNumber: result.flightNumber,
|
|
217
|
+
airlineCode: result.airlineCode,
|
|
218
|
+
airlineName: result.airlineName,
|
|
219
|
+
airStatus: result.airStatus,
|
|
220
|
+
extra: result.extra
|
|
221
|
+
};
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error instanceof ShipsGoCreditsExhaustedError) {
|
|
224
|
+
await this.recordCreditExhaustion(em, job);
|
|
225
|
+
return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 };
|
|
226
|
+
}
|
|
227
|
+
if (error instanceof ShipsGoRateLimitedError) {
|
|
228
|
+
const backoffMs = (error.retryAfterSeconds ?? 60) * 1e3;
|
|
229
|
+
await this.recordTransientBackoff(em, job, error.message, backoffMs);
|
|
230
|
+
return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 };
|
|
231
|
+
}
|
|
232
|
+
if (error instanceof ShipsGoAuthError) {
|
|
233
|
+
await this.recordTransientBackoff(em, job, error.message, 30 * 60 * 1e3);
|
|
234
|
+
return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 };
|
|
235
|
+
}
|
|
236
|
+
const message = error instanceof Error ? error.message : "Unknown ShipsGo fetch error";
|
|
237
|
+
await this.recordJobError(em, job, message);
|
|
238
|
+
return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 };
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
const adapter = this.deps.carrierRegistry.get(job.carrierCode);
|
|
242
|
+
if (!adapter) {
|
|
243
|
+
await this.recordJobError(em, job, `No adapter registered for carrier: ${job.carrierCode}`);
|
|
244
|
+
return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 };
|
|
245
|
+
}
|
|
246
|
+
const carrierConfig = await findOneWithDecryption(
|
|
247
|
+
em,
|
|
248
|
+
CarrierConfig,
|
|
249
|
+
{
|
|
250
|
+
carrierCode: job.carrierCode,
|
|
251
|
+
organizationId: job.organizationId,
|
|
252
|
+
tenantId: job.tenantId,
|
|
253
|
+
isActive: true
|
|
254
|
+
},
|
|
255
|
+
void 0,
|
|
256
|
+
scope
|
|
257
|
+
);
|
|
258
|
+
if (carrierConfig) {
|
|
259
|
+
const limitResult = await checkRateLimit(
|
|
260
|
+
this.deps.cacheService,
|
|
261
|
+
job.tenantId,
|
|
262
|
+
job.carrierCode,
|
|
263
|
+
carrierConfig.rateLimitRequests,
|
|
264
|
+
carrierConfig.rateLimitWindowSeconds
|
|
265
|
+
);
|
|
266
|
+
if (!limitResult.allowed) {
|
|
267
|
+
trackingLogger.debug("Rate limited, skipping poll", {
|
|
268
|
+
trackingJobId: jobId,
|
|
269
|
+
carrierCode: job.carrierCode,
|
|
270
|
+
retryAfterSeconds: limitResult.retryAfterSeconds
|
|
271
|
+
});
|
|
272
|
+
return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const result = await adapter.fetchEvents({
|
|
277
|
+
referenceType: job.referenceType,
|
|
278
|
+
referenceValue: job.referenceValue,
|
|
279
|
+
apiEndpoint: carrierConfig?.apiEndpoint,
|
|
280
|
+
authConfig: carrierConfig?.authConfig
|
|
281
|
+
});
|
|
282
|
+
fetchedEvents = result.events;
|
|
283
|
+
carrierResult = {
|
|
284
|
+
vesselName: result.vesselName,
|
|
285
|
+
vesselImo: result.vesselImo,
|
|
286
|
+
bookingNumber: result.bookingNumber,
|
|
287
|
+
bolNumber: result.bolNumber
|
|
288
|
+
};
|
|
289
|
+
} catch (error) {
|
|
290
|
+
const message = error instanceof Error ? error.message : "Unknown fetch error";
|
|
291
|
+
await this.recordJobError(em, job, message);
|
|
292
|
+
return { newEvents: 0, shipmentsCreated: 0, shipmentsUpdated: 0 };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (fetchedEvents.length > 0 && (!job.originUnlocode || !job.destinationUnlocode)) {
|
|
296
|
+
const inferred = inferRouteFromEvents(fetchedEvents);
|
|
297
|
+
if (!job.originUnlocode && inferred.originUnlocode) {
|
|
298
|
+
job.originUnlocode = inferred.originUnlocode;
|
|
299
|
+
trackingLogger.info("Auto-inferred origin", {
|
|
300
|
+
trackingJobId: job.id,
|
|
301
|
+
carrierCode: job.carrierCode,
|
|
302
|
+
origin: inferred.originUnlocode,
|
|
303
|
+
confidence: inferred.confidence.origin
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (!job.destinationUnlocode && inferred.destinationUnlocode) {
|
|
307
|
+
job.destinationUnlocode = inferred.destinationUnlocode;
|
|
308
|
+
trackingLogger.info("Auto-inferred destination", {
|
|
309
|
+
trackingJobId: job.id,
|
|
310
|
+
carrierCode: job.carrierCode,
|
|
311
|
+
destination: inferred.destinationUnlocode,
|
|
312
|
+
confidence: inferred.confidence.destination
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const existingByKey = new Map(
|
|
317
|
+
(await em.find(TrackingEvent, {
|
|
318
|
+
trackingJob: job,
|
|
319
|
+
sourceEventId: { $in: fetchedEvents.map((e) => e.sourceEventId) }
|
|
320
|
+
})).map((e) => [`${e.source}:${e.sourceEventId}`, e])
|
|
321
|
+
);
|
|
322
|
+
const newEvents = [];
|
|
323
|
+
let estRefreshed = false;
|
|
324
|
+
for (const fetched of fetchedEvents) {
|
|
325
|
+
const dedupeKey = `${fetched.source}:${fetched.sourceEventId}`;
|
|
326
|
+
const prior = existingByKey.get(dedupeKey);
|
|
327
|
+
if (prior) {
|
|
328
|
+
if (fetched.eventClassifierCode === "EST" && prior.eventDateTime.getTime() !== fetched.eventDateTime.getTime()) {
|
|
329
|
+
prior.eventDateTime = fetched.eventDateTime;
|
|
330
|
+
prior.eventDateTimeOffset = fetched.eventDateTimeOffset ?? prior.eventDateTimeOffset;
|
|
331
|
+
prior.description = fetched.description ?? prior.description;
|
|
332
|
+
prior.rawData = fetched.rawData ?? prior.rawData;
|
|
333
|
+
estRefreshed = true;
|
|
334
|
+
}
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const trackingEvent = em.create(TrackingEvent, {
|
|
338
|
+
organizationId: job.organizationId,
|
|
339
|
+
tenantId: job.tenantId,
|
|
340
|
+
trackingJob: job,
|
|
341
|
+
// Source identification
|
|
342
|
+
source: fetched.source,
|
|
343
|
+
sourceEventId: fetched.sourceEventId,
|
|
344
|
+
// Core event fields
|
|
345
|
+
eventType: fetched.eventType,
|
|
346
|
+
eventCode: fetched.eventCode,
|
|
347
|
+
eventClassifierCode: fetched.eventClassifierCode,
|
|
348
|
+
eventDateTime: fetched.eventDateTime,
|
|
349
|
+
eventDateTimeOffset: fetched.eventDateTimeOffset,
|
|
350
|
+
description: fetched.description,
|
|
351
|
+
rawData: fetched.rawData,
|
|
352
|
+
// Equipment fields
|
|
353
|
+
equipmentReference: fetched.equipmentReference,
|
|
354
|
+
isoEquipmentCode: fetched.isoEquipmentCode,
|
|
355
|
+
emptyIndicatorCode: fetched.emptyIndicatorCode,
|
|
356
|
+
isTransshipmentMove: fetched.isTransshipmentMove,
|
|
357
|
+
// Location fields
|
|
358
|
+
locationName: fetched.locationName,
|
|
359
|
+
locationUnlocode: fetched.locationUnlocode,
|
|
360
|
+
locationCountry: fetched.locationCountry,
|
|
361
|
+
facilityCode: fetched.facilityCode,
|
|
362
|
+
facilityCodeListProvider: fetched.facilityCodeListProvider,
|
|
363
|
+
facilityTypeCode: fetched.facilityTypeCode,
|
|
364
|
+
facilityAddress: fetched.facilityAddress,
|
|
365
|
+
latitude: fetched.latitude,
|
|
366
|
+
longitude: fetched.longitude,
|
|
367
|
+
// Transport call fields
|
|
368
|
+
transportCallReference: fetched.transportCallReference,
|
|
369
|
+
modeOfTransport: fetched.modeOfTransport,
|
|
370
|
+
vesselName: fetched.vesselName,
|
|
371
|
+
vesselImo: fetched.vesselImo,
|
|
372
|
+
voyageNumber: fetched.voyageNumber,
|
|
373
|
+
carrierServiceCode: fetched.carrierServiceCode,
|
|
374
|
+
carrierExportVoyageNumber: fetched.carrierExportVoyageNumber,
|
|
375
|
+
carrierImportVoyageNumber: fetched.carrierImportVoyageNumber,
|
|
376
|
+
universalServiceReference: fetched.universalServiceReference,
|
|
377
|
+
universalExportVoyageReference: fetched.universalExportVoyageReference,
|
|
378
|
+
universalImportVoyageReference: fetched.universalImportVoyageReference,
|
|
379
|
+
portVisitReference: fetched.portVisitReference,
|
|
380
|
+
// Document references
|
|
381
|
+
relatedDocumentReferences: fetched.relatedDocumentReferences,
|
|
382
|
+
// Metadata fields
|
|
383
|
+
eventCreatedDateTime: fetched.eventCreatedDateTime,
|
|
384
|
+
retractedEventId: fetched.retractedEventId,
|
|
385
|
+
publisherName: fetched.publisherName,
|
|
386
|
+
publisherRole: fetched.publisherRole,
|
|
387
|
+
// Additional fields
|
|
388
|
+
delayReasonCode: fetched.delayReasonCode,
|
|
389
|
+
changeRemark: fetched.changeRemark,
|
|
390
|
+
seals: fetched.seals
|
|
391
|
+
});
|
|
392
|
+
newEvents.push(trackingEvent);
|
|
393
|
+
existingByKey.set(dedupeKey, trackingEvent);
|
|
394
|
+
}
|
|
395
|
+
if (newEvents.length > 0 || estRefreshed) {
|
|
396
|
+
await em.flush();
|
|
397
|
+
}
|
|
398
|
+
const bicConfig = await this.loadBicConfig(em, scope);
|
|
399
|
+
const { shipmentsCreated, shipmentsUpdated, allComplete } = await this.syncShipmentsFromEvents(
|
|
400
|
+
em,
|
|
401
|
+
job,
|
|
402
|
+
carrierResult,
|
|
403
|
+
bicConfig
|
|
404
|
+
);
|
|
405
|
+
job.lastPollAt = /* @__PURE__ */ new Date();
|
|
406
|
+
job.retryCount = 0;
|
|
407
|
+
await em.flush();
|
|
408
|
+
for (const event of newEvents) {
|
|
409
|
+
await this.emitTrackingEventCreated(event, job);
|
|
410
|
+
}
|
|
411
|
+
if (allComplete && job.status === "active") {
|
|
412
|
+
await this.stopJobInline(em, job, "completed");
|
|
413
|
+
trackingLogger.info("Tracking job completed (empty return / delivery, or stale after destination arrival)", {
|
|
414
|
+
trackingJobId: job.id,
|
|
415
|
+
tenantId: job.tenantId,
|
|
416
|
+
organizationId: job.organizationId
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
return { newEvents: newEvents.length, shipmentsCreated, shipmentsUpdated };
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Syncs Shipments based on TrackingEvents for a job.
|
|
423
|
+
* Auto-discovers containers from equipmentReference in EQUIPMENT events.
|
|
424
|
+
* For container-based tracking (referenceType='container'), creates exactly one Shipment.
|
|
425
|
+
* For booking/BOL tracking, creates one Shipment per unique equipmentReference.
|
|
426
|
+
*/
|
|
427
|
+
/**
|
|
428
|
+
* Air counterpart to syncShipmentsFromEvents. Air has no container/equipment
|
|
429
|
+
* model, so it maintains exactly one AWB-keyed Shipment (mode='air') per job,
|
|
430
|
+
* fills flight/airline metadata, and derives timestamps + status from the air
|
|
431
|
+
* movements (via deriveShipmentStateFromEvents with the ShipsGo top-level
|
|
432
|
+
* airStatus). The job auto-completes once the shipment is DELIVERED.
|
|
433
|
+
*/
|
|
434
|
+
async syncAirShipmentFromEvents(em, job, carrierResult, bicConfig) {
|
|
435
|
+
const allEvents = await em.find(
|
|
436
|
+
TrackingEvent,
|
|
437
|
+
{ trackingJob: job },
|
|
438
|
+
{ orderBy: { eventDateTime: "asc" } }
|
|
439
|
+
);
|
|
440
|
+
const existing = await em.find(Shipment, { trackingJob: job, deletedAt: null });
|
|
441
|
+
let shipment = existing[0];
|
|
442
|
+
let shipmentsCreated = 0;
|
|
443
|
+
let isNew = false;
|
|
444
|
+
if (!shipment) {
|
|
445
|
+
shipment = em.create(Shipment, {
|
|
446
|
+
organizationId: job.organizationId,
|
|
447
|
+
tenantId: job.tenantId,
|
|
448
|
+
trackingJob: job,
|
|
449
|
+
carrierCode: job.carrierCode,
|
|
450
|
+
mode: "air",
|
|
451
|
+
awbNumber: carrierResult.awbNumber ?? job.referenceValue,
|
|
452
|
+
originLocation: job.originUnlocode ? createBasicLocation(job.originUnlocode) : null,
|
|
453
|
+
destinationLocation: job.destinationUnlocode ? createBasicLocation(job.destinationUnlocode) : null,
|
|
454
|
+
status: deriveAirShipmentStatus(carrierResult.airStatus, "PENDING")
|
|
455
|
+
});
|
|
456
|
+
em.persist(shipment);
|
|
457
|
+
shipmentsCreated = 1;
|
|
458
|
+
isNew = true;
|
|
459
|
+
trackingLogger.info("Auto-created air Shipment", {
|
|
460
|
+
shipmentId: shipment.id,
|
|
461
|
+
awbNumber: shipment.awbNumber,
|
|
462
|
+
trackingJobId: job.id
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (carrierResult.flightNumber) shipment.flightNumber = carrierResult.flightNumber;
|
|
466
|
+
if (carrierResult.airlineCode) shipment.airlineCode = carrierResult.airlineCode;
|
|
467
|
+
if (carrierResult.airlineName) shipment.airlineName = carrierResult.airlineName;
|
|
468
|
+
if (!shipment.awbNumber && carrierResult.awbNumber) shipment.awbNumber = carrierResult.awbNumber;
|
|
469
|
+
if (carrierResult.extra) {
|
|
470
|
+
shipment.extra = { ...shipment.extra ?? {}, ...carrierResult.extra };
|
|
471
|
+
}
|
|
472
|
+
const statusChanges = [];
|
|
473
|
+
const etaChanges = [];
|
|
474
|
+
const etdChanges = [];
|
|
475
|
+
let shipmentsUpdated = 0;
|
|
476
|
+
if (allEvents.length > 0) {
|
|
477
|
+
const result = await this.deriveShipmentStateFromEvents(
|
|
478
|
+
em,
|
|
479
|
+
shipment,
|
|
480
|
+
allEvents,
|
|
481
|
+
bicConfig,
|
|
482
|
+
carrierResult.airStatus ?? null
|
|
483
|
+
);
|
|
484
|
+
if (result.changed) {
|
|
485
|
+
shipmentsUpdated = 1;
|
|
486
|
+
if (result.statusChange) statusChanges.push({ shipment, ...result.statusChange });
|
|
487
|
+
if (result.etaChange) etaChanges.push({ shipment, ...result.etaChange });
|
|
488
|
+
if (result.etdChange) etdChanges.push({ shipment, ...result.etdChange });
|
|
489
|
+
}
|
|
490
|
+
} else if (carrierResult.airStatus) {
|
|
491
|
+
const next = deriveAirShipmentStatus(carrierResult.airStatus, shipment.status);
|
|
492
|
+
if (next !== shipment.status) {
|
|
493
|
+
statusChanges.push({ shipment, previousStatus: shipment.status, newStatus: next });
|
|
494
|
+
shipment.status = next;
|
|
495
|
+
shipmentsUpdated = 1;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
await em.flush();
|
|
499
|
+
if (isNew) {
|
|
500
|
+
await this.deps.eventBus.emit("shipment_tracking.shipment.created", {
|
|
501
|
+
id: shipment.id,
|
|
502
|
+
awbNumber: shipment.awbNumber,
|
|
503
|
+
trackingJobId: job.id,
|
|
504
|
+
tenantId: job.tenantId,
|
|
505
|
+
organizationId: job.organizationId
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
await this.emitShipmentDerivedChanges([shipment], statusChanges, etaChanges, etdChanges);
|
|
509
|
+
const latestEventAt = allEvents.length > 0 ? allEvents[allEvents.length - 1].eventDateTime : null;
|
|
510
|
+
const staleArrived = shipment.status === "ARRIVED" && latestEventAt != null && latestEventAt < staleArrivalCutoff();
|
|
511
|
+
return {
|
|
512
|
+
shipmentsCreated,
|
|
513
|
+
shipmentsUpdated,
|
|
514
|
+
allComplete: shipment.status === "DELIVERED" || staleArrived
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
async syncShipmentsFromEvents(em, job, carrierResult, bicConfig) {
|
|
518
|
+
if (job.mode === "air") {
|
|
519
|
+
return this.syncAirShipmentFromEvents(em, job, carrierResult, bicConfig);
|
|
520
|
+
}
|
|
521
|
+
const allEvents = await em.find(
|
|
522
|
+
TrackingEvent,
|
|
523
|
+
{ trackingJob: job },
|
|
524
|
+
{ orderBy: { eventDateTime: "asc" } }
|
|
525
|
+
);
|
|
526
|
+
if (allEvents.length === 0) {
|
|
527
|
+
return { shipmentsCreated: 0, shipmentsUpdated: 0, allComplete: false };
|
|
528
|
+
}
|
|
529
|
+
const containerNumbers = /* @__PURE__ */ new Set();
|
|
530
|
+
for (const event of allEvents) {
|
|
531
|
+
if (event.eventType === "EQUIPMENT" && event.equipmentReference) {
|
|
532
|
+
containerNumbers.add(event.equipmentReference);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (containerNumbers.size === 0 && job.referenceType === "container") {
|
|
536
|
+
containerNumbers.add(job.referenceValue);
|
|
537
|
+
}
|
|
538
|
+
if (containerNumbers.size === 0) {
|
|
539
|
+
return { shipmentsCreated: 0, shipmentsUpdated: 0, allComplete: false };
|
|
540
|
+
}
|
|
541
|
+
const existingShipments = await em.find(Shipment, {
|
|
542
|
+
trackingJob: job,
|
|
543
|
+
deletedAt: null
|
|
544
|
+
});
|
|
545
|
+
const shipmentsByContainer = new Map(
|
|
546
|
+
existingShipments.map((s) => [s.containerNumber, s])
|
|
547
|
+
);
|
|
548
|
+
let shipmentsCreated = 0;
|
|
549
|
+
let shipmentsUpdated = 0;
|
|
550
|
+
const newShipments = [];
|
|
551
|
+
const statusChanges = [];
|
|
552
|
+
const etaChanges = [];
|
|
553
|
+
const etdChanges = [];
|
|
554
|
+
for (const containerNumber of containerNumbers) {
|
|
555
|
+
let shipment = shipmentsByContainer.get(containerNumber);
|
|
556
|
+
if (!shipment) {
|
|
557
|
+
shipment = em.create(Shipment, {
|
|
558
|
+
organizationId: job.organizationId,
|
|
559
|
+
tenantId: job.tenantId,
|
|
560
|
+
trackingJob: job,
|
|
561
|
+
carrierCode: job.carrierCode,
|
|
562
|
+
containerNumber,
|
|
563
|
+
bookingNumber: job.referenceType === "booking" ? job.referenceValue : carrierResult.bookingNumber,
|
|
564
|
+
bolNumber: job.referenceType === "bol" ? job.referenceValue : carrierResult.bolNumber,
|
|
565
|
+
// Initialize with basic location from job's UN/LOCODE if available
|
|
566
|
+
// Will be enriched with full facility data from events later
|
|
567
|
+
originLocation: job.originUnlocode ? createBasicLocation(job.originUnlocode) : null,
|
|
568
|
+
destinationLocation: job.destinationUnlocode ? createBasicLocation(job.destinationUnlocode) : null,
|
|
569
|
+
status: "PENDING"
|
|
570
|
+
});
|
|
571
|
+
em.persist(shipment);
|
|
572
|
+
shipmentsByContainer.set(containerNumber, shipment);
|
|
573
|
+
shipmentsCreated++;
|
|
574
|
+
newShipments.push({ shipment, containerNumber });
|
|
575
|
+
trackingLogger.info("Auto-created Shipment", {
|
|
576
|
+
shipmentId: shipment.id,
|
|
577
|
+
containerNumber,
|
|
578
|
+
trackingJobId: job.id,
|
|
579
|
+
carrierCode: job.carrierCode
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
if (!shipment.bookingNumber && carrierResult.bookingNumber) {
|
|
583
|
+
shipment.bookingNumber = carrierResult.bookingNumber;
|
|
584
|
+
}
|
|
585
|
+
if (!shipment.bolNumber && carrierResult.bolNumber) {
|
|
586
|
+
shipment.bolNumber = carrierResult.bolNumber;
|
|
587
|
+
}
|
|
588
|
+
if (carrierResult.extra) {
|
|
589
|
+
shipment.extra = { ...shipment.extra ?? {}, ...carrierResult.extra };
|
|
590
|
+
}
|
|
591
|
+
const result = await this.deriveShipmentStateFromEvents(em, shipment, allEvents, bicConfig);
|
|
592
|
+
if (result.changed) {
|
|
593
|
+
shipmentsUpdated++;
|
|
594
|
+
if (result.statusChange) {
|
|
595
|
+
statusChanges.push({
|
|
596
|
+
shipment,
|
|
597
|
+
previousStatus: result.statusChange.previousStatus,
|
|
598
|
+
newStatus: result.statusChange.newStatus
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
if (result.etaChange) {
|
|
602
|
+
etaChanges.push({
|
|
603
|
+
shipment,
|
|
604
|
+
previousEta: result.etaChange.previousEta,
|
|
605
|
+
newEta: result.etaChange.newEta
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
if (result.etdChange) {
|
|
609
|
+
etdChanges.push({
|
|
610
|
+
shipment,
|
|
611
|
+
previousEtd: result.etdChange.previousEtd,
|
|
612
|
+
newEtd: result.etdChange.newEtd
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
await em.flush();
|
|
618
|
+
for (const { shipment, containerNumber } of newShipments) {
|
|
619
|
+
await this.deps.eventBus.emit("shipment_tracking.shipment.created", {
|
|
620
|
+
id: shipment.id,
|
|
621
|
+
containerNumber,
|
|
622
|
+
trackingJobId: job.id,
|
|
623
|
+
tenantId: job.tenantId,
|
|
624
|
+
organizationId: job.organizationId
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
const polledShipments = [...containerNumbers].map((cn) => shipmentsByContainer.get(cn)).filter((s) => s != null);
|
|
628
|
+
await this.emitShipmentDerivedChanges(polledShipments, statusChanges, etaChanges, etdChanges);
|
|
629
|
+
const allComplete = allContainersComplete(allEvents, containerNumbers) || destinationArrivedBefore(allEvents, job.destinationUnlocode, staleArrivalCutoff());
|
|
630
|
+
return { shipmentsCreated, shipmentsUpdated, allComplete };
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Emit the shipment status/lifecycle/timestamp change events produced by a round of
|
|
634
|
+
* deriveShipmentStateFromEvents. Shared by the carrier poll (syncShipmentsFromEvents)
|
|
635
|
+
* and the real-time AIS refresh (refreshShipmentsFromEvents) so both paths surface the
|
|
636
|
+
* same downstream events. Call AFTER flushing so subscribers can fetch committed rows.
|
|
637
|
+
*
|
|
638
|
+
* @param updatedShipments - shipments to emit a bare `shipment.updated` for when they
|
|
639
|
+
* did not also produce a status change (status changes already emit `updated`).
|
|
640
|
+
*/
|
|
641
|
+
async emitShipmentDerivedChanges(updatedShipments, statusChanges, etaChanges, etdChanges) {
|
|
642
|
+
for (const { shipment, previousStatus, newStatus } of statusChanges) {
|
|
643
|
+
await this.deps.eventBus.emit("shipment_tracking.shipment.status_changed", {
|
|
644
|
+
id: shipment.id,
|
|
645
|
+
previousStatus,
|
|
646
|
+
newStatus,
|
|
647
|
+
tenantId: shipment.tenantId,
|
|
648
|
+
organizationId: shipment.organizationId
|
|
649
|
+
});
|
|
650
|
+
if (newStatus === "BOOKED" && previousStatus === "PENDING") {
|
|
651
|
+
await this.deps.eventBus.emit("shipment_tracking.shipment.booked", {
|
|
652
|
+
id: shipment.id,
|
|
653
|
+
tenantId: shipment.tenantId,
|
|
654
|
+
organizationId: shipment.organizationId
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
if (newStatus === "PRE_ARRIVAL") {
|
|
658
|
+
await this.deps.eventBus.emit("shipment_tracking.shipment.pre_arrival", {
|
|
659
|
+
id: shipment.id,
|
|
660
|
+
tenantId: shipment.tenantId,
|
|
661
|
+
organizationId: shipment.organizationId
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
if (newStatus === "DELIVERED") {
|
|
665
|
+
await this.deps.eventBus.emit("shipment_tracking.shipment.delivered", {
|
|
666
|
+
id: shipment.id,
|
|
667
|
+
tenantId: shipment.tenantId,
|
|
668
|
+
organizationId: shipment.organizationId
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
await this.deps.eventBus.emit("shipment_tracking.shipment.updated", {
|
|
672
|
+
id: shipment.id,
|
|
673
|
+
tenantId: shipment.tenantId,
|
|
674
|
+
organizationId: shipment.organizationId
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
const alreadyEmittedIds = new Set(statusChanges.map((sc) => sc.shipment.id));
|
|
678
|
+
for (const shipment of updatedShipments) {
|
|
679
|
+
if (!alreadyEmittedIds.has(shipment.id)) {
|
|
680
|
+
await this.deps.eventBus.emit("shipment_tracking.shipment.updated", {
|
|
681
|
+
id: shipment.id,
|
|
682
|
+
tenantId: shipment.tenantId,
|
|
683
|
+
organizationId: shipment.organizationId
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
for (const { shipment, previousEta, newEta } of etaChanges) {
|
|
688
|
+
await this.deps.eventBus.emit("shipment_tracking.transport.eta_updated", {
|
|
689
|
+
id: shipment.id,
|
|
690
|
+
previousEta: previousEta.toISOString(),
|
|
691
|
+
newEta: newEta.toISOString(),
|
|
692
|
+
tenantId: shipment.tenantId,
|
|
693
|
+
organizationId: shipment.organizationId
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
for (const { shipment, previousEtd, newEtd } of etdChanges) {
|
|
697
|
+
await this.deps.eventBus.emit("shipment_tracking.transport.etd_updated", {
|
|
698
|
+
id: shipment.id,
|
|
699
|
+
previousEtd: previousEtd.toISOString(),
|
|
700
|
+
newEtd: newEtd.toISOString(),
|
|
701
|
+
tenantId: shipment.tenantId,
|
|
702
|
+
organizationId: shipment.organizationId
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Re-derive denormalized state (cargoEvents, routeStops, status, ETA/ETD/ATA/ATD, seals,
|
|
708
|
+
* locations) for a tracking job's shipments from their persisted TrackingEvents, then emit
|
|
709
|
+
* the resulting change events. Used by real-time ingestion (e.g. AIS POI events over NATS)
|
|
710
|
+
* so the Journey Timeline and derived state update immediately instead of waiting for the
|
|
711
|
+
* next carrier poll. Idempotent: deriveShipmentStateFromEvents is a no-op when nothing changed.
|
|
712
|
+
*
|
|
713
|
+
* @param em - entity manager managing the (already flushed) new TrackingEvents
|
|
714
|
+
* @param trackingJobId - the job whose shipments to refresh
|
|
715
|
+
* @param shipmentIds - optional subset; defaults to all of the job's shipments
|
|
716
|
+
*/
|
|
717
|
+
async refreshShipmentsFromEvents(em, trackingJobId, shipmentIds) {
|
|
718
|
+
const job = await em.findOne(TrackingJob, { id: trackingJobId });
|
|
719
|
+
if (!job) return;
|
|
720
|
+
if (job.mode === "air") return;
|
|
721
|
+
const shipments = shipmentIds && shipmentIds.length > 0 ? await em.find(Shipment, { trackingJob: job, deletedAt: null, id: { $in: shipmentIds } }) : await em.find(Shipment, { trackingJob: job, deletedAt: null });
|
|
722
|
+
if (shipments.length === 0) return;
|
|
723
|
+
const allEvents = await em.find(
|
|
724
|
+
TrackingEvent,
|
|
725
|
+
{ trackingJob: job },
|
|
726
|
+
{ orderBy: { eventDateTime: "asc" } }
|
|
727
|
+
);
|
|
728
|
+
if (allEvents.length === 0) return;
|
|
729
|
+
const bicConfig = await this.loadBicConfig(em, {
|
|
730
|
+
organizationId: job.organizationId,
|
|
731
|
+
tenantId: job.tenantId
|
|
732
|
+
});
|
|
733
|
+
const statusChanges = [];
|
|
734
|
+
const etaChanges = [];
|
|
735
|
+
const etdChanges = [];
|
|
736
|
+
const changedShipments = [];
|
|
737
|
+
for (const shipment of shipments) {
|
|
738
|
+
const result = await this.deriveShipmentStateFromEvents(em, shipment, allEvents, bicConfig);
|
|
739
|
+
if (!result.changed) continue;
|
|
740
|
+
changedShipments.push(shipment);
|
|
741
|
+
if (result.statusChange) statusChanges.push({ shipment, ...result.statusChange });
|
|
742
|
+
if (result.etaChange) etaChanges.push({ shipment, ...result.etaChange });
|
|
743
|
+
if (result.etdChange) etdChanges.push({ shipment, ...result.etdChange });
|
|
744
|
+
}
|
|
745
|
+
if (changedShipments.length === 0) return;
|
|
746
|
+
await em.flush();
|
|
747
|
+
await this.emitShipmentDerivedChanges(changedShipments, statusChanges, etaChanges, etdChanges);
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Derives and updates Shipment state from TrackingEvents.
|
|
751
|
+
* Filters events by equipmentReference to get container-specific events.
|
|
752
|
+
* TRANSPORT events (no equipmentReference) apply to all containers.
|
|
753
|
+
*
|
|
754
|
+
* Returns status change info and time changes so caller can emit events after flush.
|
|
755
|
+
*/
|
|
756
|
+
async deriveShipmentStateFromEvents(em, shipment, allEvents, bicConfig, airStatus) {
|
|
757
|
+
const containerEvents = allEvents.filter((event) => {
|
|
758
|
+
if (event.eventType === "TRANSPORT") return true;
|
|
759
|
+
if (event.eventType === "EQUIPMENT") {
|
|
760
|
+
return event.equipmentReference === shipment.containerNumber;
|
|
761
|
+
}
|
|
762
|
+
return false;
|
|
763
|
+
});
|
|
764
|
+
if (containerEvents.length === 0) {
|
|
765
|
+
return { changed: false };
|
|
766
|
+
}
|
|
767
|
+
const originUnlocode = shipment.originLocation?.unlocode ?? null;
|
|
768
|
+
const destinationUnlocode = shipment.destinationLocation?.unlocode ?? null;
|
|
769
|
+
const context = {
|
|
770
|
+
originUnlocode,
|
|
771
|
+
destinationUnlocode
|
|
772
|
+
};
|
|
773
|
+
const times = extractShipmentTimes(
|
|
774
|
+
containerEvents.map((event) => ({
|
|
775
|
+
eventCode: event.eventCode,
|
|
776
|
+
eventClassifierCode: event.eventClassifierCode,
|
|
777
|
+
eventDateTime: event.eventDateTime,
|
|
778
|
+
eventDateTimeOffset: event.eventDateTimeOffset,
|
|
779
|
+
eventCreatedDateTime: event.eventCreatedDateTime,
|
|
780
|
+
locationUnlocode: event.locationUnlocode
|
|
781
|
+
})),
|
|
782
|
+
context
|
|
783
|
+
);
|
|
784
|
+
const latestEvent = containerEvents[containerEvents.length - 1];
|
|
785
|
+
const latestVesselInfo = findLatestVesselInfo(containerEvents);
|
|
786
|
+
const previousEta = getPrimaryTimestampValue(shipment.etaTimestamps);
|
|
787
|
+
const previousEtd = getPrimaryTimestampValue(shipment.etdTimestamps);
|
|
788
|
+
const previousAtd = getPrimaryTimestampValue(shipment.atdTimestamps);
|
|
789
|
+
const previousAta = getPrimaryTimestampValue(shipment.ataTimestamps);
|
|
790
|
+
const mergedTimestamps = mergeExtractedTimestamps(
|
|
791
|
+
{
|
|
792
|
+
etdTimestamps: shipment.etdTimestamps,
|
|
793
|
+
etaTimestamps: shipment.etaTimestamps,
|
|
794
|
+
atdTimestamps: shipment.atdTimestamps,
|
|
795
|
+
ataTimestamps: shipment.ataTimestamps
|
|
796
|
+
},
|
|
797
|
+
times,
|
|
798
|
+
"carrier_api",
|
|
799
|
+
latestEvent?.sourceEventId
|
|
800
|
+
);
|
|
801
|
+
const newEta = getPrimaryTimestampValue(mergedTimestamps.etaTimestamps);
|
|
802
|
+
const newEtd = getPrimaryTimestampValue(mergedTimestamps.etdTimestamps);
|
|
803
|
+
const newAtd = getPrimaryTimestampValue(mergedTimestamps.atdTimestamps);
|
|
804
|
+
const newAta = getPrimaryTimestampValue(mergedTimestamps.ataTimestamps);
|
|
805
|
+
const previousStatus = shipment.status;
|
|
806
|
+
const newStatus = airStatus !== void 0 ? deriveAirShipmentStatus(airStatus, shipment.status) : deriveShipmentStatus(
|
|
807
|
+
containerEvents.map((event) => ({
|
|
808
|
+
eventCode: event.eventCode,
|
|
809
|
+
eventClassifierCode: event.eventClassifierCode,
|
|
810
|
+
locationUnlocode: event.locationUnlocode
|
|
811
|
+
})),
|
|
812
|
+
context,
|
|
813
|
+
shipment.status,
|
|
814
|
+
{ eta: newEta, ata: newAta }
|
|
815
|
+
);
|
|
816
|
+
const etaChanged = newEta && previousEta && newEta.getTime() !== previousEta.getTime();
|
|
817
|
+
const etdChanged = newEtd && previousEtd && newEtd.getTime() !== previousEtd.getTime();
|
|
818
|
+
const changed = previousStatus !== newStatus || mergedTimestamps.changed || newEtd && !previousEtd || etdChanged || newEta && !previousEta || etaChanged || newAtd && !previousAtd || newAtd && previousAtd && newAtd.getTime() !== previousAtd.getTime() || newAta && !previousAta || newAta && previousAta && newAta.getTime() !== previousAta.getTime() || shipment.vesselName !== latestEvent?.vesselName || containerEvents.length !== shipment.eventCount;
|
|
819
|
+
if (!changed) {
|
|
820
|
+
return { changed: false };
|
|
821
|
+
}
|
|
822
|
+
shipment.status = newStatus;
|
|
823
|
+
shipment.etdTimestamps = mergedTimestamps.etdTimestamps;
|
|
824
|
+
shipment.etaTimestamps = mergedTimestamps.etaTimestamps;
|
|
825
|
+
shipment.atdTimestamps = mergedTimestamps.atdTimestamps;
|
|
826
|
+
shipment.ataTimestamps = mergedTimestamps.ataTimestamps;
|
|
827
|
+
if (latestVesselInfo) {
|
|
828
|
+
shipment.vesselName = latestVesselInfo.vesselName ?? shipment.vesselName;
|
|
829
|
+
shipment.vesselImo = latestVesselInfo.vesselImo ?? shipment.vesselImo;
|
|
830
|
+
shipment.voyageNumber = latestVesselInfo.voyageNumber ?? shipment.voyageNumber;
|
|
831
|
+
}
|
|
832
|
+
if (!shipment.isoEquipmentCode) {
|
|
833
|
+
const eventWithIsoCode = containerEvents.find((e) => e.isoEquipmentCode);
|
|
834
|
+
if (eventWithIsoCode) {
|
|
835
|
+
shipment.isoEquipmentCode = eventWithIsoCode.isoEquipmentCode;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (!shipment.bookingNumber) {
|
|
839
|
+
for (const event of containerEvents) {
|
|
840
|
+
const bkgRef = event.relatedDocumentReferences?.find((ref) => ref.type === "BKG");
|
|
841
|
+
if (bkgRef?.value) {
|
|
842
|
+
shipment.bookingNumber = bkgRef.value;
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (!shipment.bolNumber) {
|
|
848
|
+
for (const event of containerEvents) {
|
|
849
|
+
const bolRef = event.relatedDocumentReferences?.find((ref) => ref.type === "TRD" || ref.type === "SHI");
|
|
850
|
+
if (bolRef?.value) {
|
|
851
|
+
shipment.bolNumber = bolRef.value;
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (latestEvent) {
|
|
857
|
+
shipment.lastEventAt = latestEvent.eventDateTime;
|
|
858
|
+
}
|
|
859
|
+
shipment.eventCount = containerEvents.length;
|
|
860
|
+
const cargoEvents = containerEvents.map(mapTrackingEventToEntry);
|
|
861
|
+
shipment.cargoEvents = cargoEvents;
|
|
862
|
+
shipment.routeStops = extractRouteFromEvents(cargoEvents, {
|
|
863
|
+
originUnlocode,
|
|
864
|
+
destinationUnlocode
|
|
865
|
+
});
|
|
866
|
+
const allSeals = containerEvents.flatMap((e) => e.seals ?? []).filter((seal) => seal.number);
|
|
867
|
+
if (allSeals.length > 0) {
|
|
868
|
+
const seenSeals = /* @__PURE__ */ new Map();
|
|
869
|
+
for (const seal of allSeals) {
|
|
870
|
+
seenSeals.set(seal.number, seal);
|
|
871
|
+
}
|
|
872
|
+
shipment.seals = Array.from(seenSeals.values());
|
|
873
|
+
} else {
|
|
874
|
+
shipment.seals = null;
|
|
875
|
+
}
|
|
876
|
+
const originEventsAtLocation = containerEvents.filter(
|
|
877
|
+
(e) => e.locationUnlocode === originUnlocode
|
|
878
|
+
);
|
|
879
|
+
const originEvent = (
|
|
880
|
+
// Priority 1: LOAD event at origin
|
|
881
|
+
originEventsAtLocation.find((e) => e.eventCode === "LOAD" && e.eventClassifierCode === "ACT") ?? // Priority 2: Any event with facility code at origin
|
|
882
|
+
originEventsAtLocation.find((e) => e.facilityCode != null) ?? // Fallback: First event at origin, or LOAD event anywhere if no origin specified
|
|
883
|
+
originEventsAtLocation[0] ?? (!originUnlocode ? containerEvents.find((e) => e.eventCode === "LOAD" && e.eventClassifierCode === "ACT") : null)
|
|
884
|
+
);
|
|
885
|
+
const destEventsAtLocation = containerEvents.filter(
|
|
886
|
+
(e) => e.locationUnlocode === destinationUnlocode
|
|
887
|
+
);
|
|
888
|
+
const destEventsReversed = [...destEventsAtLocation].reverse();
|
|
889
|
+
const destEvent = (
|
|
890
|
+
// Priority 1: DISC event at destination (last one)
|
|
891
|
+
destEventsReversed.find((e) => e.eventCode === "DISC" && e.eventClassifierCode === "ACT") ?? // Priority 2: Any event with facility code at destination (last one)
|
|
892
|
+
destEventsReversed.find((e) => e.facilityCode != null) ?? // Fallback: Last event at destination, or DISC/ARRI event anywhere if no destination specified
|
|
893
|
+
destEventsReversed[0] ?? (!destinationUnlocode ? [...containerEvents].reverse().find((e) => (e.eventCode === "DISC" || e.eventCode === "ARRI") && e.eventClassifierCode === "ACT") : null)
|
|
894
|
+
);
|
|
895
|
+
if (originEvent) {
|
|
896
|
+
shipment.originLocation = buildLocationFromEvent({
|
|
897
|
+
locationName: originEvent.locationName,
|
|
898
|
+
locationUnlocode: originEvent.locationUnlocode,
|
|
899
|
+
locationCountry: originEvent.locationCountry,
|
|
900
|
+
facilityCode: originEvent.facilityCode,
|
|
901
|
+
facilityCodeListProvider: originEvent.facilityCodeListProvider,
|
|
902
|
+
facilityTypeCode: originEvent.facilityTypeCode,
|
|
903
|
+
facilityAddress: originEvent.facilityAddress,
|
|
904
|
+
latitude: originEvent.latitude,
|
|
905
|
+
longitude: originEvent.longitude
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
if (destEvent) {
|
|
909
|
+
shipment.destinationLocation = buildLocationFromEvent({
|
|
910
|
+
locationName: destEvent.locationName,
|
|
911
|
+
locationUnlocode: destEvent.locationUnlocode,
|
|
912
|
+
locationCountry: destEvent.locationCountry,
|
|
913
|
+
facilityCode: destEvent.facilityCode,
|
|
914
|
+
facilityCodeListProvider: destEvent.facilityCodeListProvider,
|
|
915
|
+
facilityTypeCode: destEvent.facilityTypeCode,
|
|
916
|
+
facilityAddress: destEvent.facilityAddress,
|
|
917
|
+
latitude: destEvent.latitude,
|
|
918
|
+
longitude: destEvent.longitude
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
if (bicConfig) {
|
|
922
|
+
await this.enrichShipmentLocationsWithBic(shipment, containerEvents, bicConfig);
|
|
923
|
+
}
|
|
924
|
+
const scope = { organizationId: shipment.organizationId, tenantId: shipment.tenantId };
|
|
925
|
+
const carrierCode = shipment.carrierCode?.toUpperCase() ?? null;
|
|
926
|
+
shipment.originLocation = await applyLocationOverrideIfExists(
|
|
927
|
+
em,
|
|
928
|
+
shipment.originLocation,
|
|
929
|
+
carrierCode,
|
|
930
|
+
scope
|
|
931
|
+
);
|
|
932
|
+
shipment.destinationLocation = await applyLocationOverrideIfExists(
|
|
933
|
+
em,
|
|
934
|
+
shipment.destinationLocation,
|
|
935
|
+
carrierCode,
|
|
936
|
+
scope
|
|
937
|
+
);
|
|
938
|
+
if (shipment.routeStops) {
|
|
939
|
+
for (const stop of shipment.routeStops) {
|
|
940
|
+
if (stop.facilityCode && stop.facilityCodeListProvider) {
|
|
941
|
+
const overridden = await applyLocationOverrideIfExists(em, {
|
|
942
|
+
name: stop.location,
|
|
943
|
+
unlocode: stop.unlocode ?? null,
|
|
944
|
+
countryCode: stop.unlocode?.slice(0, 2) ?? null,
|
|
945
|
+
facilityCode: stop.facilityCode,
|
|
946
|
+
facilityCodeListProvider: stop.facilityCodeListProvider,
|
|
947
|
+
facilityTypeCode: stop.facilityTypeCode ?? null,
|
|
948
|
+
address: stop.facilityAddress ?? null,
|
|
949
|
+
coords: stop.coords ?? null,
|
|
950
|
+
operatorName: null,
|
|
951
|
+
source: "dcsa"
|
|
952
|
+
}, carrierCode, scope);
|
|
953
|
+
if (overridden && overridden.source === "manual") {
|
|
954
|
+
stop.location = overridden.name;
|
|
955
|
+
stop.facilityAddress = overridden.address;
|
|
956
|
+
stop.coords = overridden.coords;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
const statusChange = previousStatus !== newStatus ? { previousStatus, newStatus } : void 0;
|
|
962
|
+
const etaChange = etaChanged && previousEta && newEta ? { previousEta, newEta } : void 0;
|
|
963
|
+
const etdChange = etdChanged && previousEtd && newEtd ? { previousEtd, newEtd } : void 0;
|
|
964
|
+
return { changed: true, statusChange, etaChange, etdChange };
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Emits events for a newly created TrackingEvent.
|
|
968
|
+
*/
|
|
969
|
+
async emitTrackingEventCreated(event, job) {
|
|
970
|
+
const eventPayload = {
|
|
971
|
+
id: event.id,
|
|
972
|
+
trackingJobId: job.id,
|
|
973
|
+
tenantId: event.tenantId,
|
|
974
|
+
organizationId: event.organizationId,
|
|
975
|
+
// Source
|
|
976
|
+
source: event.source,
|
|
977
|
+
sourceEventId: event.sourceEventId,
|
|
978
|
+
// Core event fields
|
|
979
|
+
eventType: event.eventType,
|
|
980
|
+
eventCode: event.eventCode,
|
|
981
|
+
eventClassifierCode: event.eventClassifierCode,
|
|
982
|
+
eventDateTime: event.eventDateTime?.toISOString(),
|
|
983
|
+
description: event.description,
|
|
984
|
+
// Equipment fields
|
|
985
|
+
equipmentReference: event.equipmentReference,
|
|
986
|
+
isoEquipmentCode: event.isoEquipmentCode,
|
|
987
|
+
emptyIndicatorCode: event.emptyIndicatorCode,
|
|
988
|
+
isTransshipmentMove: event.isTransshipmentMove,
|
|
989
|
+
// Location fields
|
|
990
|
+
locationName: event.locationName,
|
|
991
|
+
locationUnlocode: event.locationUnlocode,
|
|
992
|
+
locationCountry: event.locationCountry,
|
|
993
|
+
facilityCode: event.facilityCode,
|
|
994
|
+
facilityTypeCode: event.facilityTypeCode,
|
|
995
|
+
// Transport call fields
|
|
996
|
+
vesselName: event.vesselName,
|
|
997
|
+
vesselImo: event.vesselImo,
|
|
998
|
+
voyageNumber: event.voyageNumber,
|
|
999
|
+
carrierServiceCode: event.carrierServiceCode,
|
|
1000
|
+
modeOfTransport: event.modeOfTransport,
|
|
1001
|
+
// Document references
|
|
1002
|
+
relatedDocumentReferences: event.relatedDocumentReferences,
|
|
1003
|
+
// Metadata
|
|
1004
|
+
publisherName: event.publisherName,
|
|
1005
|
+
publisherRole: event.publisherRole
|
|
1006
|
+
};
|
|
1007
|
+
await this.deps.eventBus.emit("shipment_tracking.tracking_event.created", eventPayload);
|
|
1008
|
+
if (isSignificantMilestone({
|
|
1009
|
+
eventType: event.eventType,
|
|
1010
|
+
eventCode: event.eventCode,
|
|
1011
|
+
eventClassifierCode: event.eventClassifierCode
|
|
1012
|
+
})) {
|
|
1013
|
+
const dcsaEventType = mapDcsaEventToWebhookType({
|
|
1014
|
+
eventType: event.eventType,
|
|
1015
|
+
eventCode: event.eventCode,
|
|
1016
|
+
eventClassifierCode: event.eventClassifierCode
|
|
1017
|
+
});
|
|
1018
|
+
await this.deps.eventBus.emit(dcsaEventType, eventPayload);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Loads BIC Facility API configuration for a tenant/organization.
|
|
1023
|
+
*/
|
|
1024
|
+
async loadBicConfig(em, scope) {
|
|
1025
|
+
return findOneWithDecryption(
|
|
1026
|
+
em,
|
|
1027
|
+
BicConfig,
|
|
1028
|
+
{
|
|
1029
|
+
organizationId: scope.organizationId,
|
|
1030
|
+
tenantId: scope.tenantId,
|
|
1031
|
+
isEnabled: true
|
|
1032
|
+
},
|
|
1033
|
+
void 0,
|
|
1034
|
+
scope
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Enriches shipment origin/destination locations and route stops with BIC Facility API data.
|
|
1039
|
+
* Only fetches data for facilities that are incomplete (missing coords or address).
|
|
1040
|
+
* One API call per unique facility code per poll.
|
|
1041
|
+
*/
|
|
1042
|
+
async enrichShipmentLocationsWithBic(shipment, events, bicConfig) {
|
|
1043
|
+
const facilitiesToEnrich = /* @__PURE__ */ new Map();
|
|
1044
|
+
if (shipment.originLocation && !isLocationComplete(shipment.originLocation) && shipment.originLocation.facilityCode) {
|
|
1045
|
+
const provider = shipment.originLocation.facilityCodeListProvider ?? "SMDG";
|
|
1046
|
+
facilitiesToEnrich.set(shipment.originLocation.facilityCode, {
|
|
1047
|
+
code: shipment.originLocation.facilityCode,
|
|
1048
|
+
provider,
|
|
1049
|
+
unlocode: shipment.originLocation.unlocode ?? ""
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
if (shipment.destinationLocation && !isLocationComplete(shipment.destinationLocation) && shipment.destinationLocation.facilityCode) {
|
|
1053
|
+
const provider = shipment.destinationLocation.facilityCodeListProvider ?? "SMDG";
|
|
1054
|
+
facilitiesToEnrich.set(shipment.destinationLocation.facilityCode, {
|
|
1055
|
+
code: shipment.destinationLocation.facilityCode,
|
|
1056
|
+
provider,
|
|
1057
|
+
unlocode: shipment.destinationLocation.unlocode ?? ""
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
for (const event of events) {
|
|
1061
|
+
if (event.facilityCode && event.latitude == null && !event.facilityAddress) {
|
|
1062
|
+
const provider = event.facilityCodeListProvider ?? "SMDG";
|
|
1063
|
+
if (!facilitiesToEnrich.has(event.facilityCode)) {
|
|
1064
|
+
facilitiesToEnrich.set(event.facilityCode, {
|
|
1065
|
+
code: event.facilityCode,
|
|
1066
|
+
provider,
|
|
1067
|
+
unlocode: event.locationUnlocode ?? ""
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
if (facilitiesToEnrich.size === 0) {
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const client = new BicApiClient({
|
|
1076
|
+
baseUrl: bicConfig.baseUrl,
|
|
1077
|
+
username: bicConfig.username,
|
|
1078
|
+
password: bicConfig.password
|
|
1079
|
+
});
|
|
1080
|
+
const facilityMap = /* @__PURE__ */ new Map();
|
|
1081
|
+
for (const [code, info] of facilitiesToEnrich) {
|
|
1082
|
+
try {
|
|
1083
|
+
const facility = await client.getFacility(info.code, info.provider, info.unlocode);
|
|
1084
|
+
if (facility) {
|
|
1085
|
+
facilityMap.set(code, facility);
|
|
1086
|
+
}
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
trackingLogger.error("Failed to fetch BIC facility", {
|
|
1089
|
+
facilityCode: code,
|
|
1090
|
+
codeProvider: info.provider,
|
|
1091
|
+
unlocode: info.unlocode,
|
|
1092
|
+
shipmentId: shipment.id,
|
|
1093
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (facilityMap.size === 0) {
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
if (shipment.originLocation?.facilityCode && facilityMap.has(shipment.originLocation.facilityCode)) {
|
|
1101
|
+
const bic = facilityMap.get(shipment.originLocation.facilityCode);
|
|
1102
|
+
shipment.originLocation = this.mergeLocationWithBicFacility(shipment.originLocation, bic);
|
|
1103
|
+
}
|
|
1104
|
+
if (shipment.destinationLocation?.facilityCode && facilityMap.has(shipment.destinationLocation.facilityCode)) {
|
|
1105
|
+
const bic = facilityMap.get(shipment.destinationLocation.facilityCode);
|
|
1106
|
+
shipment.destinationLocation = this.mergeLocationWithBicFacility(shipment.destinationLocation, bic);
|
|
1107
|
+
}
|
|
1108
|
+
if (shipment.routeStops) {
|
|
1109
|
+
for (const stop of shipment.routeStops) {
|
|
1110
|
+
if (stop.facilityCode && facilityMap.has(stop.facilityCode)) {
|
|
1111
|
+
const bic = facilityMap.get(stop.facilityCode);
|
|
1112
|
+
const coords = BicApiClient.parseCoordinates(bic);
|
|
1113
|
+
const address = BicApiClient.formatAddress(bic);
|
|
1114
|
+
if (!stop.coords && coords) {
|
|
1115
|
+
stop.coords = coords;
|
|
1116
|
+
}
|
|
1117
|
+
if (!stop.facilityAddress && address) {
|
|
1118
|
+
stop.facilityAddress = address;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if (shipment.cargoEvents) {
|
|
1124
|
+
for (const event of shipment.cargoEvents) {
|
|
1125
|
+
if (event.facilityCode && facilityMap.has(event.facilityCode)) {
|
|
1126
|
+
const bic = facilityMap.get(event.facilityCode);
|
|
1127
|
+
const coords = BicApiClient.parseCoordinates(bic);
|
|
1128
|
+
const address = BicApiClient.formatAddress(bic);
|
|
1129
|
+
if (event.latitude == null && coords) {
|
|
1130
|
+
event.latitude = coords.latitude;
|
|
1131
|
+
event.longitude = coords.longitude;
|
|
1132
|
+
}
|
|
1133
|
+
if (!event.facilityAddress && address) {
|
|
1134
|
+
event.facilityAddress = address;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Merges BIC facility data into a FacilityLocation (only fills missing fields).
|
|
1142
|
+
*/
|
|
1143
|
+
mergeLocationWithBicFacility(location, bic) {
|
|
1144
|
+
const coords = BicApiClient.parseCoordinates(bic);
|
|
1145
|
+
const address = BicApiClient.formatAddress(bic);
|
|
1146
|
+
const operatorName = BicApiClient.getOperatorName(bic);
|
|
1147
|
+
const facilityName = BicApiClient.getFacilityName(bic);
|
|
1148
|
+
return mergeLocationWithBicData(location, {
|
|
1149
|
+
name: facilityName ?? void 0,
|
|
1150
|
+
address: address ?? void 0,
|
|
1151
|
+
coords: coords ?? void 0,
|
|
1152
|
+
operatorName: operatorName ?? void 0
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Stops an active tracking job (sets status to 'deactivated').
|
|
1157
|
+
* Tenant-scoped: refuses to stop a job not belonging to the caller's tenant.
|
|
1158
|
+
* Idempotent: returns { stopped: false } when the job is already inactive,
|
|
1159
|
+
* doesn't exist, or belongs to a different tenant.
|
|
1160
|
+
*/
|
|
1161
|
+
async stopTrackingJob(jobId, ctx) {
|
|
1162
|
+
const em = this.deps.em();
|
|
1163
|
+
const job = await em.findOne(TrackingJob, {
|
|
1164
|
+
id: jobId,
|
|
1165
|
+
organizationId: ctx.organizationId,
|
|
1166
|
+
tenantId: ctx.tenantId,
|
|
1167
|
+
deletedAt: null
|
|
1168
|
+
});
|
|
1169
|
+
if (!job) {
|
|
1170
|
+
return { stopped: false };
|
|
1171
|
+
}
|
|
1172
|
+
if (job.status !== "active" && job.status !== "paused") {
|
|
1173
|
+
return { stopped: false };
|
|
1174
|
+
}
|
|
1175
|
+
await this.stopJobInline(em, job);
|
|
1176
|
+
trackingLogger.info("Tracking job stopped", {
|
|
1177
|
+
trackingJobId: jobId,
|
|
1178
|
+
tenantId: ctx.tenantId,
|
|
1179
|
+
organizationId: ctx.organizationId
|
|
1180
|
+
});
|
|
1181
|
+
return { stopped: true };
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Stop a job (target status defaults to `deactivated`) and emit
|
|
1185
|
+
* `tracking_job.completed`, operating on the given EntityManager (the job must
|
|
1186
|
+
* be managed by it). Shared by the public `stopTrackingJob` (externally
|
|
1187
|
+
* stopped → `deactivated`) and the carrier-poll auto-stop (cargo finished →
|
|
1188
|
+
* `completed`) so both routes flush + emit identically. Either status is
|
|
1189
|
+
* excluded from the `status: 'active'` poll filter, so polling stops.
|
|
1190
|
+
*/
|
|
1191
|
+
async stopJobInline(em, job, status = "deactivated") {
|
|
1192
|
+
job.status = status;
|
|
1193
|
+
job.nextPollAt = null;
|
|
1194
|
+
await em.flush();
|
|
1195
|
+
await this.deps.eventBus.emit("shipment_tracking.tracking_job.completed", {
|
|
1196
|
+
id: job.id,
|
|
1197
|
+
tenantId: job.tenantId,
|
|
1198
|
+
organizationId: job.organizationId
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Reconciliation: stops any active TrackingJob that has no folder_legs row
|
|
1203
|
+
* referencing it via tracking_job_id. Used by the daily reconciliation cron
|
|
1204
|
+
* to clean up orphans created by failed Phase B/C re-tracking flows.
|
|
1205
|
+
*
|
|
1206
|
+
* Returns { stoppedCount } so the cron can surface to ops.
|
|
1207
|
+
* The optional folderLegsTracker param exists so this code can run from
|
|
1208
|
+
* @freighttech/shipment-tracking without a hard import on @freighttech/projects:
|
|
1209
|
+
* the caller passes a function that returns the set of tracking_job_ids
|
|
1210
|
+
* currently referenced by folder_legs.
|
|
1211
|
+
*/
|
|
1212
|
+
async reconcileOrphanedTrackingJobs(folderLegTrackingJobIds, ctx) {
|
|
1213
|
+
const em = this.deps.em();
|
|
1214
|
+
const filter = {
|
|
1215
|
+
status: "active",
|
|
1216
|
+
deletedAt: null
|
|
1217
|
+
};
|
|
1218
|
+
if (ctx?.organizationId) filter.organizationId = ctx.organizationId;
|
|
1219
|
+
if (ctx?.tenantId) filter.tenantId = ctx.tenantId;
|
|
1220
|
+
const activeJobs = await em.find(TrackingJob, filter);
|
|
1221
|
+
let stoppedCount = 0;
|
|
1222
|
+
for (const job of activeJobs) {
|
|
1223
|
+
if (folderLegTrackingJobIds.has(job.id)) continue;
|
|
1224
|
+
const result = await this.stopTrackingJob(job.id, {
|
|
1225
|
+
organizationId: job.organizationId,
|
|
1226
|
+
tenantId: job.tenantId
|
|
1227
|
+
});
|
|
1228
|
+
if (result.stopped) stoppedCount++;
|
|
1229
|
+
}
|
|
1230
|
+
return { stoppedCount, checked: activeJobs.length };
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Records an error on a tracking job.
|
|
1234
|
+
*/
|
|
1235
|
+
/**
|
|
1236
|
+
* Backs a job off by `backoffMs` and logs `message` in its error history,
|
|
1237
|
+
* WITHOUT touching retryCount. For account-level/transient ShipsGo conditions
|
|
1238
|
+
* (402 credits, 429 rate limit, 403 auth) that must not consume the per-job
|
|
1239
|
+
* failure budget (which would flip the job to 'failed') nor keep re-hitting an
|
|
1240
|
+
* exhausted/throttled account every poll. The job resumes on its own.
|
|
1241
|
+
*/
|
|
1242
|
+
async recordTransientBackoff(em, job, message, backoffMs) {
|
|
1243
|
+
const history = job.errorHistory ?? [];
|
|
1244
|
+
history.push({ date: (/* @__PURE__ */ new Date()).toISOString(), message });
|
|
1245
|
+
if (history.length > 20) history.splice(0, history.length - 20);
|
|
1246
|
+
job.errorHistory = history;
|
|
1247
|
+
job.lastPollAt = /* @__PURE__ */ new Date();
|
|
1248
|
+
job.nextPollAt = new Date(Date.now() + backoffMs);
|
|
1249
|
+
await em.flush();
|
|
1250
|
+
}
|
|
1251
|
+
/** ShipsGo credit-exhaustion (HTTP 402): back off ~6h until credits are topped up. */
|
|
1252
|
+
async recordCreditExhaustion(em, job) {
|
|
1253
|
+
await this.recordTransientBackoff(em, job, "ShipsGo credits exhausted (HTTP 402)", 6 * 60 * 60 * 1e3);
|
|
1254
|
+
}
|
|
1255
|
+
async recordJobError(em, job, message) {
|
|
1256
|
+
const history = job.errorHistory ?? [];
|
|
1257
|
+
history.push({ date: (/* @__PURE__ */ new Date()).toISOString(), message });
|
|
1258
|
+
if (history.length > 20) {
|
|
1259
|
+
history.splice(0, history.length - 20);
|
|
1260
|
+
}
|
|
1261
|
+
job.errorHistory = history;
|
|
1262
|
+
job.retryCount = (job.retryCount || 0) + 1;
|
|
1263
|
+
job.lastPollAt = /* @__PURE__ */ new Date();
|
|
1264
|
+
if (job.retryCount >= 10) {
|
|
1265
|
+
job.status = "failed";
|
|
1266
|
+
await em.flush();
|
|
1267
|
+
await this.deps.eventBus.emit("shipment_tracking.tracking_job.failed", {
|
|
1268
|
+
id: job.id,
|
|
1269
|
+
carrierCode: job.carrierCode,
|
|
1270
|
+
retryCount: job.retryCount,
|
|
1271
|
+
lastError: message,
|
|
1272
|
+
tenantId: job.tenantId,
|
|
1273
|
+
organizationId: job.organizationId
|
|
1274
|
+
});
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
await em.flush();
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
export {
|
|
1281
|
+
TrackingService
|
|
1282
|
+
};
|
|
1283
|
+
//# sourceMappingURL=trackingService.js.map
|