@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,143 @@
|
|
|
1
|
+
const TOKEN_HEADER = "X-Shipsgo-User-Token";
|
|
2
|
+
const REQUEST_TIMEOUT_MS = 2e4;
|
|
3
|
+
function normalizeAwbNumber(raw) {
|
|
4
|
+
const digits = raw.replace(/\D/g, "");
|
|
5
|
+
if (digits.length === 11) return `${digits.slice(0, 3)}-${digits.slice(3)}`;
|
|
6
|
+
return raw.trim();
|
|
7
|
+
}
|
|
8
|
+
class ShipsGoCreditsExhaustedError extends Error {
|
|
9
|
+
constructor(message = "ShipsGo credits exhausted (HTTP 402)") {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "ShipsGoCreditsExhaustedError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
class ShipsGoAuthError extends Error {
|
|
15
|
+
constructor(message = "ShipsGo authentication failed (HTTP 403)") {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "ShipsGoAuthError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
class ShipsGoRateLimitedError extends Error {
|
|
21
|
+
constructor(message = "ShipsGo rate limit exceeded (HTTP 429)", retryAfterSeconds2) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.retryAfterSeconds = retryAfterSeconds2;
|
|
24
|
+
this.name = "ShipsGoRateLimitedError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
class ShipsGoApiError extends Error {
|
|
28
|
+
constructor(status, message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.status = status;
|
|
31
|
+
this.name = "ShipsGoApiError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function shipsGoFetch(config, path, init) {
|
|
35
|
+
const url = `${config.baseUrl.replace(/\/$/, "")}${path}`;
|
|
36
|
+
return fetch(url, {
|
|
37
|
+
...init,
|
|
38
|
+
signal: init.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
39
|
+
headers: {
|
|
40
|
+
[TOKEN_HEADER]: config.apiToken,
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
Accept: "application/json",
|
|
43
|
+
...init.headers ?? {}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function throwForAccountLevelStatus(response) {
|
|
48
|
+
if (response.status === 402) throw new ShipsGoCreditsExhaustedError();
|
|
49
|
+
if (response.status === 403) throw new ShipsGoAuthError();
|
|
50
|
+
if (response.status === 429) throw new ShipsGoRateLimitedError(void 0, retryAfterSeconds(response));
|
|
51
|
+
}
|
|
52
|
+
function retryAfterSeconds(response) {
|
|
53
|
+
const reset = response.headers.get("RateLimit-Reset") ?? response.headers.get("X-RateLimit-Reset");
|
|
54
|
+
if (reset) {
|
|
55
|
+
const resetEpoch = Number(reset);
|
|
56
|
+
if (Number.isFinite(resetEpoch)) return Math.max(0, Math.ceil(resetEpoch - Date.now() / 1e3));
|
|
57
|
+
}
|
|
58
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
59
|
+
if (retryAfter) {
|
|
60
|
+
const secs = Number(retryAfter);
|
|
61
|
+
if (Number.isFinite(secs)) return Math.max(0, Math.ceil(secs));
|
|
62
|
+
}
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
function toId(shipment) {
|
|
66
|
+
if (!shipment || shipment.id == null) {
|
|
67
|
+
throw new ShipsGoApiError(500, "ShipsGo response missing shipment id");
|
|
68
|
+
}
|
|
69
|
+
return String(shipment.id);
|
|
70
|
+
}
|
|
71
|
+
async function handleRegister(response) {
|
|
72
|
+
throwForAccountLevelStatus(response);
|
|
73
|
+
if (response.status === 409) {
|
|
74
|
+
const body2 = await response.json().catch(() => ({}));
|
|
75
|
+
return { id: toId(body2.shipment), alreadyExisted: true };
|
|
76
|
+
}
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const text = await response.text().catch(() => "unknown");
|
|
79
|
+
throw new ShipsGoApiError(response.status, `ShipsGo register failed (${response.status}): ${text}`);
|
|
80
|
+
}
|
|
81
|
+
const body = await response.json();
|
|
82
|
+
return { id: toId(body.shipment), alreadyExisted: false };
|
|
83
|
+
}
|
|
84
|
+
async function registerOceanShipment(config, input) {
|
|
85
|
+
const payload = { reference: input.reference };
|
|
86
|
+
if (input.bookingNumber) payload.booking_number = input.bookingNumber;
|
|
87
|
+
else if (input.containerNumber) payload.container_number = input.containerNumber;
|
|
88
|
+
if (input.carrier) payload.carrier = input.carrier;
|
|
89
|
+
const response = await shipsGoFetch(config, "/ocean/shipments", {
|
|
90
|
+
method: "POST",
|
|
91
|
+
body: JSON.stringify(payload)
|
|
92
|
+
});
|
|
93
|
+
return handleRegister(response);
|
|
94
|
+
}
|
|
95
|
+
async function getOceanShipment(config, id) {
|
|
96
|
+
const response = await shipsGoFetch(config, `/ocean/shipments/${encodeURIComponent(id)}`, {
|
|
97
|
+
method: "GET"
|
|
98
|
+
});
|
|
99
|
+
throwForAccountLevelStatus(response);
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
const text = await response.text().catch(() => "unknown");
|
|
102
|
+
throw new ShipsGoApiError(response.status, `ShipsGo get ocean shipment failed (${response.status}): ${text}`);
|
|
103
|
+
}
|
|
104
|
+
const body = await response.json();
|
|
105
|
+
if (!body.shipment) {
|
|
106
|
+
throw new ShipsGoApiError(500, "ShipsGo ocean response missing shipment");
|
|
107
|
+
}
|
|
108
|
+
return body.shipment;
|
|
109
|
+
}
|
|
110
|
+
async function registerAirShipment(config, input) {
|
|
111
|
+
const response = await shipsGoFetch(config, "/air/shipments", {
|
|
112
|
+
method: "POST",
|
|
113
|
+
body: JSON.stringify({ reference: input.reference, awb_number: normalizeAwbNumber(input.awbNumber) })
|
|
114
|
+
});
|
|
115
|
+
return handleRegister(response);
|
|
116
|
+
}
|
|
117
|
+
async function getAirShipment(config, id) {
|
|
118
|
+
const response = await shipsGoFetch(config, `/air/shipments/${encodeURIComponent(id)}`, {
|
|
119
|
+
method: "GET"
|
|
120
|
+
});
|
|
121
|
+
throwForAccountLevelStatus(response);
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
const text = await response.text().catch(() => "unknown");
|
|
124
|
+
throw new ShipsGoApiError(response.status, `ShipsGo get air shipment failed (${response.status}): ${text}`);
|
|
125
|
+
}
|
|
126
|
+
const body = await response.json();
|
|
127
|
+
if (!body.shipment) {
|
|
128
|
+
throw new ShipsGoApiError(500, "ShipsGo air response missing shipment");
|
|
129
|
+
}
|
|
130
|
+
return body.shipment;
|
|
131
|
+
}
|
|
132
|
+
export {
|
|
133
|
+
ShipsGoApiError,
|
|
134
|
+
ShipsGoAuthError,
|
|
135
|
+
ShipsGoCreditsExhaustedError,
|
|
136
|
+
ShipsGoRateLimitedError,
|
|
137
|
+
getAirShipment,
|
|
138
|
+
getOceanShipment,
|
|
139
|
+
normalizeAwbNumber,
|
|
140
|
+
registerAirShipment,
|
|
141
|
+
registerOceanShipment
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=shipsgo-client.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/shipment_tracking/lib/shipsgo-client.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * ShipsGo API v2 client (ocean + air).\n *\n * ShipsGo is a register-then-poll provider: you POST a shipment (spending a\n * credit) and receive a ShipsGo shipment id, then GET that id for movements.\n * Registration is idempotent on `reference` \u2014 a duplicate returns 409 with the\n * existing shipment, so we treat 200 and 409 the same (both yield an id).\n *\n * Raw `fetch` is correct here (this is a server-side lib, not UI \u2014 `apiCall`\n * is the frontend helper). Auth is the `X-Shipsgo-User-Token` header.\n *\n * Docs: https://api.shipsgo.com/v2\n */\n\nconst TOKEN_HEADER = 'X-Shipsgo-User-Token'\n\n/**\n * Per-request timeout. ShipsGo polls run inside a sequential job loop, so a hung\n * endpoint would otherwise stall the whole worker. Aborted requests surface as a\n * normal fetch error \u2192 recorded as a transient job error.\n */\nconst REQUEST_TIMEOUT_MS = 20_000\n\n/**\n * Standardizes an AWB to ShipsGo's `NNN-NNNNNNNN` shape (3 digits, hyphen, 8\n * digits) so a value typed with spaces/other separators still validates against\n * ShipsGo's `^[0-9]{3}(-)?[0-9]{8}$` pattern. Returns the trimmed input\n * unchanged when it isn't 11 digits (let ShipsGo return the authoritative error).\n */\nexport function normalizeAwbNumber(raw: string): string {\n const digits = raw.replace(/\\D/g, '')\n if (digits.length === 11) return `${digits.slice(0, 3)}-${digits.slice(3)}`\n return raw.trim()\n}\n\nexport type ShipsGoClientConfig = {\n apiToken: string\n baseUrl: string\n}\n\n/** Thrown on HTTP 402 \u2014 the ShipsGo account has no remaining credits. */\nexport class ShipsGoCreditsExhaustedError extends Error {\n constructor(message = 'ShipsGo credits exhausted (HTTP 402)') {\n super(message)\n this.name = 'ShipsGoCreditsExhaustedError'\n }\n}\n\n/**\n * Thrown on HTTP 403 \u2014 the token is missing/invalid/expired. This is an\n * account-level condition (like 402), not a per-job fault, so callers back the\n * job off instead of burning its retry budget.\n */\nexport class ShipsGoAuthError extends Error {\n constructor(message = 'ShipsGo authentication failed (HTTP 403)') {\n super(message)\n this.name = 'ShipsGoAuthError'\n }\n}\n\n/**\n * Thrown on HTTP 429 \u2014 the ShipsGo rate limit (100 req/min, collective across\n * the account) was exceeded. `retryAfterSeconds` is derived from the\n * `RateLimit-Reset` (UTC epoch) or `Retry-After` header when present. Transient,\n * so callers back the job off without consuming its retry budget.\n */\nexport class ShipsGoRateLimitedError extends Error {\n constructor(\n message = 'ShipsGo rate limit exceeded (HTTP 429)',\n public readonly retryAfterSeconds?: number,\n ) {\n super(message)\n this.name = 'ShipsGoRateLimitedError'\n }\n}\n\n/** Thrown for any other non-2xx (and non-409) ShipsGo response. */\nexport class ShipsGoApiError extends Error {\n constructor(\n public readonly status: number,\n message: string,\n ) {\n super(message)\n this.name = 'ShipsGoApiError'\n }\n}\n\nexport type ShipsGoRegisterResult = {\n /** ShipsGo shipment id (stringified integer). */\n id: string\n /** True when the shipment already existed (HTTP 409) \u2014 no credit spent. */\n alreadyExisted: boolean\n}\n\n// \u2500\u2500\u2500 Response types (only the fields we consume) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type ShipsGoOceanLocation = {\n code?: string | null\n name?: string | null\n timezone?: string | null\n country?: { code?: string | null; name?: string | null } | null\n}\n\nexport type ShipsGoOceanMovement = {\n event: string // EMSH | GTIN | LOAD | DEPA | ARRV | DISC | GTOT | EMRT\n status: 'EST' | 'ACT'\n location?: ShipsGoOceanLocation | null\n vessel?: { imo?: number | null; name?: string | null } | null\n voyage?: string | null\n timestamp: string\n}\n\nexport type ShipsGoOceanContainer = {\n number: string\n status?: string | null\n size?: number | null\n type?: string | null\n movements?: ShipsGoOceanMovement[] | null\n}\n\n/** ShipsGo route summary (shipment-level; POL/POD, transit, emissions). */\nexport type ShipsGoOceanRoutePort = {\n location?: ShipsGoOceanLocation | null\n date_of_loading?: string | null\n date_of_discharge?: string | null\n date_of_discharge_predicted?: string | null\n}\n\nexport type ShipsGoOceanRoute = {\n port_of_loading?: ShipsGoOceanRoutePort | null\n port_of_discharge?: ShipsGoOceanRoutePort | null\n transit_time?: number | null\n transit_percentage?: number | null\n co2_emission?: number | null\n ts_count?: number | null\n}\n\nexport type ShipsGoOceanShipment = {\n id: number\n reference?: string | null\n booking_number?: string | null\n container_number?: string | null\n carrier?: { scac?: string | null; name?: string | null; status?: string | null } | null\n status?: string | null\n route?: ShipsGoOceanRoute | null\n containers?: ShipsGoOceanContainer[] | null\n}\n\n// \u2500\u2500\u2500 Air response types (only the fields we consume) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type ShipsGoAirLocation = {\n name?: string | null\n iata?: string | null\n timezone?: string | null\n country?: { code?: string | null; name?: string | null } | null\n}\n\nexport type ShipsGoAirMovement = {\n event: string // RCS | MAN | DEP | ARR | RCF | DLV\n status: 'EST' | 'ACT'\n location?: ShipsGoAirLocation | null\n flight?: string | null\n timestamp: string\n}\n\n/** ShipsGo air cargo summary (pieces / weight / volume). */\nexport type ShipsGoAirCargo = {\n pieces?: number | null\n weight?: number | null\n weight_unit?: string | null\n volume?: number | null\n volume_unit?: string | null\n}\n\n/** ShipsGo air route summary (origin/destination airports + transit/emissions). */\nexport type ShipsGoAirRoutePort = {\n location?: ShipsGoAirLocation | null\n date_of_dep?: string | null\n date_of_rcf?: string | null\n}\n\nexport type ShipsGoAirRoute = {\n origin?: ShipsGoAirRoutePort | null\n destination?: ShipsGoAirRoutePort | null\n transit_time?: number | null\n transit_percentage?: number | null\n co2_emission?: number | null\n ts_count?: number | null\n}\n\nexport type ShipsGoAirShipment = {\n id: number\n reference?: string | null\n awb_number?: string | null\n airline?: { iata?: string | null; name?: string | null } | null\n status?: string | null // NEW | INPROGRESS | BOOKED | EN_ROUTE | LANDED | DELIVERED | UNTRACKED\n status_extended?: unknown | null\n cargo?: ShipsGoAirCargo | null\n route?: ShipsGoAirRoute | null\n movements?: ShipsGoAirMovement[] | null\n}\n\ntype ShipmentEnvelope<T> = { message?: string; shipment?: T }\n\n// \u2500\u2500\u2500 Internal fetch helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync function shipsGoFetch(\n config: ShipsGoClientConfig,\n path: string,\n init: RequestInit,\n): Promise<Response> {\n const url = `${config.baseUrl.replace(/\\/$/, '')}${path}`\n return fetch(url, {\n ...init,\n signal: init.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n headers: {\n [TOKEN_HEADER]: config.apiToken,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n ...(init.headers ?? {}),\n },\n })\n}\n\n/**\n * Maps account-level HTTP statuses (402/403/429) to their typed errors so every\n * caller (register + GET) handles them uniformly. Returns without throwing for\n * anything else; the caller then handles 409 / generic failures itself.\n */\nfunction throwForAccountLevelStatus(response: Response): void {\n if (response.status === 402) throw new ShipsGoCreditsExhaustedError()\n if (response.status === 403) throw new ShipsGoAuthError()\n if (response.status === 429) throw new ShipsGoRateLimitedError(undefined, retryAfterSeconds(response))\n}\n\n/** Seconds until the rate-limit window resets, from `RateLimit-Reset` (UTC epoch) or `Retry-After`. */\nfunction retryAfterSeconds(response: Response): number | undefined {\n const reset = response.headers.get('RateLimit-Reset') ?? response.headers.get('X-RateLimit-Reset')\n if (reset) {\n const resetEpoch = Number(reset)\n if (Number.isFinite(resetEpoch)) return Math.max(0, Math.ceil(resetEpoch - Date.now() / 1000))\n }\n const retryAfter = response.headers.get('Retry-After')\n if (retryAfter) {\n const secs = Number(retryAfter)\n if (Number.isFinite(secs)) return Math.max(0, Math.ceil(secs))\n }\n return undefined\n}\n\nfunction toId(shipment: { id?: number } | undefined): string {\n if (!shipment || shipment.id == null) {\n throw new ShipsGoApiError(500, 'ShipsGo response missing shipment id')\n }\n return String(shipment.id)\n}\n\nasync function handleRegister(response: Response): Promise<ShipsGoRegisterResult> {\n throwForAccountLevelStatus(response)\n if (response.status === 409) {\n // Already exists \u2014 free; extract the existing shipment id.\n const body = (await response.json().catch(() => ({}))) as ShipmentEnvelope<{ id: number }>\n return { id: toId(body.shipment), alreadyExisted: true }\n }\n if (!response.ok) {\n const text = await response.text().catch(() => 'unknown')\n throw new ShipsGoApiError(response.status, `ShipsGo register failed (${response.status}): ${text}`)\n }\n const body = (await response.json()) as ShipmentEnvelope<{ id: number }>\n return { id: toId(body.shipment), alreadyExisted: false }\n}\n\n// \u2500\u2500\u2500 Ocean \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport async function registerOceanShipment(\n config: ShipsGoClientConfig,\n input: {\n reference: string\n bookingNumber?: string | null\n containerNumber?: string | null\n /** Optional SCAC hint \u2014 ShipsGo auto-detects the line when omitted. */\n carrier?: string | null\n },\n): Promise<ShipsGoRegisterResult> {\n const payload: Record<string, unknown> = { reference: input.reference }\n if (input.bookingNumber) payload.booking_number = input.bookingNumber\n else if (input.containerNumber) payload.container_number = input.containerNumber\n if (input.carrier) payload.carrier = input.carrier\n\n const response = await shipsGoFetch(config, '/ocean/shipments', {\n method: 'POST',\n body: JSON.stringify(payload),\n })\n return handleRegister(response)\n}\n\nexport async function getOceanShipment(\n config: ShipsGoClientConfig,\n id: string,\n): Promise<ShipsGoOceanShipment> {\n const response = await shipsGoFetch(config, `/ocean/shipments/${encodeURIComponent(id)}`, {\n method: 'GET',\n })\n throwForAccountLevelStatus(response)\n if (!response.ok) {\n const text = await response.text().catch(() => 'unknown')\n throw new ShipsGoApiError(response.status, `ShipsGo get ocean shipment failed (${response.status}): ${text}`)\n }\n const body = (await response.json()) as ShipmentEnvelope<ShipsGoOceanShipment>\n if (!body.shipment) {\n throw new ShipsGoApiError(500, 'ShipsGo ocean response missing shipment')\n }\n return body.shipment\n}\n\n// \u2500\u2500\u2500 Air \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport async function registerAirShipment(\n config: ShipsGoClientConfig,\n input: {\n reference: string\n /** Air Waybill number \u2014 ShipsGo standardizes to `333-88888888`. */\n awbNumber: string\n },\n): Promise<ShipsGoRegisterResult> {\n const response = await shipsGoFetch(config, '/air/shipments', {\n method: 'POST',\n body: JSON.stringify({ reference: input.reference, awb_number: normalizeAwbNumber(input.awbNumber) }),\n })\n return handleRegister(response)\n}\n\nexport async function getAirShipment(\n config: ShipsGoClientConfig,\n id: string,\n): Promise<ShipsGoAirShipment> {\n const response = await shipsGoFetch(config, `/air/shipments/${encodeURIComponent(id)}`, {\n method: 'GET',\n })\n throwForAccountLevelStatus(response)\n if (!response.ok) {\n const text = await response.text().catch(() => 'unknown')\n throw new ShipsGoApiError(response.status, `ShipsGo get air shipment failed (${response.status}): ${text}`)\n }\n const body = (await response.json()) as ShipmentEnvelope<ShipsGoAirShipment>\n if (!body.shipment) {\n throw new ShipsGoApiError(500, 'ShipsGo air response missing shipment')\n }\n return body.shipment\n}\n"],
|
|
5
|
+
"mappings": "AAcA,MAAM,eAAe;AAOrB,MAAM,qBAAqB;AAQpB,SAAS,mBAAmB,KAAqB;AACtD,QAAM,SAAS,IAAI,QAAQ,OAAO,EAAE;AACpC,MAAI,OAAO,WAAW,GAAI,QAAO,GAAG,OAAO,MAAM,GAAG,CAAC,CAAC,IAAI,OAAO,MAAM,CAAC,CAAC;AACzE,SAAO,IAAI,KAAK;AAClB;AAQO,MAAM,qCAAqC,MAAM;AAAA,EACtD,YAAY,UAAU,wCAAwC;AAC5D,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAOO,MAAM,yBAAyB,MAAM;AAAA,EAC1C,YAAY,UAAU,4CAA4C;AAChE,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAQO,MAAM,gCAAgC,MAAM;AAAA,EACjD,YACE,UAAU,0CACMA,oBAChB;AACA,UAAM,OAAO;AAFG,6BAAAA;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAGO,MAAM,wBAAwB,MAAM;AAAA,EACzC,YACkB,QAChB,SACA;AACA,UAAM,OAAO;AAHG;AAIhB,SAAK,OAAO;AAAA,EACd;AACF;AAyHA,eAAe,aACb,QACA,MACA,MACmB;AACnB,QAAM,MAAM,GAAG,OAAO,QAAQ,QAAQ,OAAO,EAAE,CAAC,GAAG,IAAI;AACvD,SAAO,MAAM,KAAK;AAAA,IAChB,GAAG;AAAA,IACH,QAAQ,KAAK,UAAU,YAAY,QAAQ,kBAAkB;AAAA,IAC7D,SAAS;AAAA,MACP,CAAC,YAAY,GAAG,OAAO;AAAA,MACvB,gBAAgB;AAAA,MAChB,QAAQ;AAAA,MACR,GAAI,KAAK,WAAW,CAAC;AAAA,IACvB;AAAA,EACF,CAAC;AACH;AAOA,SAAS,2BAA2B,UAA0B;AAC5D,MAAI,SAAS,WAAW,IAAK,OAAM,IAAI,6BAA6B;AACpE,MAAI,SAAS,WAAW,IAAK,OAAM,IAAI,iBAAiB;AACxD,MAAI,SAAS,WAAW,IAAK,OAAM,IAAI,wBAAwB,QAAW,kBAAkB,QAAQ,CAAC;AACvG;AAGA,SAAS,kBAAkB,UAAwC;AACjE,QAAM,QAAQ,SAAS,QAAQ,IAAI,iBAAiB,KAAK,SAAS,QAAQ,IAAI,mBAAmB;AACjG,MAAI,OAAO;AACT,UAAM,aAAa,OAAO,KAAK;AAC/B,QAAI,OAAO,SAAS,UAAU,EAAG,QAAO,KAAK,IAAI,GAAG,KAAK,KAAK,aAAa,KAAK,IAAI,IAAI,GAAI,CAAC;AAAA,EAC/F;AACA,QAAM,aAAa,SAAS,QAAQ,IAAI,aAAa;AACrD,MAAI,YAAY;AACd,UAAM,OAAO,OAAO,UAAU;AAC9B,QAAI,OAAO,SAAS,IAAI,EAAG,QAAO,KAAK,IAAI,GAAG,KAAK,KAAK,IAAI,CAAC;AAAA,EAC/D;AACA,SAAO;AACT;AAEA,SAAS,KAAK,UAA+C;AAC3D,MAAI,CAAC,YAAY,SAAS,MAAM,MAAM;AACpC,UAAM,IAAI,gBAAgB,KAAK,sCAAsC;AAAA,EACvE;AACA,SAAO,OAAO,SAAS,EAAE;AAC3B;AAEA,eAAe,eAAe,UAAoD;AAChF,6BAA2B,QAAQ;AACnC,MAAI,SAAS,WAAW,KAAK;AAE3B,UAAMC,QAAQ,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACpD,WAAO,EAAE,IAAI,KAAKA,MAAK,QAAQ,GAAG,gBAAgB,KAAK;AAAA,EACzD;AACA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,OAAO,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,SAAS;AACxD,UAAM,IAAI,gBAAgB,SAAS,QAAQ,4BAA4B,SAAS,MAAM,MAAM,IAAI,EAAE;AAAA,EACpG;AACA,QAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,SAAO,EAAE,IAAI,KAAK,KAAK,QAAQ,GAAG,gBAAgB,MAAM;AAC1D;AAIA,eAAsB,sBACpB,QACA,OAOgC;AAChC,QAAM,UAAmC,EAAE,WAAW,MAAM,UAAU;AACtE,MAAI,MAAM,cAAe,SAAQ,iBAAiB,MAAM;AAAA,WAC/C,MAAM,gBAAiB,SAAQ,mBAAmB,MAAM;AACjE,MAAI,MAAM,QAAS,SAAQ,UAAU,MAAM;AAE3C,QAAM,WAAW,MAAM,aAAa,QAAQ,oBAAoB;AAAA,IAC9D,QAAQ;AAAA,IACR,MAAM,KAAK,UAAU,OAAO;AAAA,EAC9B,CAAC;AACD,SAAO,eAAe,QAAQ;AAChC;AAEA,eAAsB,iBACpB,QACA,IAC+B;AAC/B,QAAM,WAAW,MAAM,aAAa,QAAQ,oBAAoB,mBAAmB,EAAE,CAAC,IAAI;AAAA,IACxF,QAAQ;AAAA,EACV,CAAC;AACD,6BAA2B,QAAQ;AACnC,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,OAAO,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,SAAS;AACxD,UAAM,IAAI,gBAAgB,SAAS,QAAQ,sCAAsC,SAAS,MAAM,MAAM,IAAI,EAAE;AAAA,EAC9G;AACA,QAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,MAAI,CAAC,KAAK,UAAU;AAClB,UAAM,IAAI,gBAAgB,KAAK,yCAAyC;AAAA,EAC1E;AACA,SAAO,KAAK;AACd;AAIA,eAAsB,oBACpB,QACA,OAKgC;AAChC,QAAM,WAAW,MAAM,aAAa,QAAQ,kBAAkB;AAAA,IAC5D,QAAQ;AAAA,IACR,MAAM,KAAK,UAAU,EAAE,WAAW,MAAM,WAAW,YAAY,mBAAmB,MAAM,SAAS,EAAE,CAAC;AAAA,EACtG,CAAC;AACD,SAAO,eAAe,QAAQ;AAChC;AAEA,eAAsB,eACpB,QACA,IAC6B;AAC7B,QAAM,WAAW,MAAM,aAAa,QAAQ,kBAAkB,mBAAmB,EAAE,CAAC,IAAI;AAAA,IACtF,QAAQ;AAAA,EACV,CAAC;AACD,6BAA2B,QAAQ;AACnC,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,OAAO,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,SAAS;AACxD,UAAM,IAAI,gBAAgB,SAAS,QAAQ,oCAAoC,SAAS,MAAM,MAAM,IAAI,EAAE;AAAA,EAC5G;AACA,QAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,MAAI,CAAC,KAAK,UAAU;AAClB,UAAM,IAAI,gBAAgB,KAAK,uCAAuC;AAAA,EACxE;AACA,SAAO,KAAK;AACd;",
|
|
6
|
+
"names": ["retryAfterSeconds", "body"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const OCEAN_EVENT_MAP = {
|
|
2
|
+
EMSH: { eventType: "EQUIPMENT", eventCode: "PICK" },
|
|
3
|
+
// empty released to shipper
|
|
4
|
+
GTIN: { eventType: "EQUIPMENT", eventCode: "GTIN" },
|
|
5
|
+
LOAD: { eventType: "EQUIPMENT", eventCode: "LOAD" },
|
|
6
|
+
DEPA: { eventType: "TRANSPORT", eventCode: "DEPA" },
|
|
7
|
+
ARRV: { eventType: "TRANSPORT", eventCode: "ARRI" },
|
|
8
|
+
DISC: { eventType: "EQUIPMENT", eventCode: "DISC" },
|
|
9
|
+
GTOT: { eventType: "EQUIPMENT", eventCode: "GTOT" },
|
|
10
|
+
EMRT: { eventType: "EQUIPMENT", eventCode: "DROP" }
|
|
11
|
+
// empty returned to depot
|
|
12
|
+
};
|
|
13
|
+
function classifier(status) {
|
|
14
|
+
return status === "ACT" ? "ACT" : "EST";
|
|
15
|
+
}
|
|
16
|
+
function movementId(container, m) {
|
|
17
|
+
const tsKey = m.status === "EST" ? "EST" : m.timestamp;
|
|
18
|
+
return `${container}:${m.event}:${m.status}:${tsKey}`;
|
|
19
|
+
}
|
|
20
|
+
function isLocode(code) {
|
|
21
|
+
return typeof code === "string" && /^[A-Z]{2}[A-Z0-9]{3}$/.test(code);
|
|
22
|
+
}
|
|
23
|
+
function isoOffset(ts) {
|
|
24
|
+
const match = ts.match(/([+-]\d{2}:\d{2})$/);
|
|
25
|
+
if (!match || match[1] === "+00:00") return null;
|
|
26
|
+
return match[1];
|
|
27
|
+
}
|
|
28
|
+
function equipmentCode(container) {
|
|
29
|
+
const size = container.size != null ? String(container.size) : "";
|
|
30
|
+
const type = container.type ?? "";
|
|
31
|
+
const code = `${size}${type}`.trim();
|
|
32
|
+
return code || null;
|
|
33
|
+
}
|
|
34
|
+
function mapOceanMovement(container, m) {
|
|
35
|
+
const mapping = OCEAN_EVENT_MAP[m.event];
|
|
36
|
+
if (!mapping) return null;
|
|
37
|
+
const ts = new Date(m.timestamp);
|
|
38
|
+
if (Number.isNaN(ts.getTime())) return null;
|
|
39
|
+
const loc = m.location;
|
|
40
|
+
const hasVessel = Boolean(m.vessel?.name || m.vessel?.imo);
|
|
41
|
+
return {
|
|
42
|
+
source: "shipsgo",
|
|
43
|
+
sourceEventId: movementId(container.number, m),
|
|
44
|
+
eventType: mapping.eventType,
|
|
45
|
+
eventCode: mapping.eventCode,
|
|
46
|
+
eventClassifierCode: classifier(m.status),
|
|
47
|
+
eventDateTime: ts,
|
|
48
|
+
eventDateTimeOffset: isoOffset(m.timestamp),
|
|
49
|
+
description: `ShipsGo ${m.event} (${m.status})`,
|
|
50
|
+
rawData: { provider: "shipsgo", ...m },
|
|
51
|
+
equipmentReference: container.number,
|
|
52
|
+
isoEquipmentCode: equipmentCode(container),
|
|
53
|
+
locationName: loc?.name ?? null,
|
|
54
|
+
locationUnlocode: isLocode(loc?.code) ? loc?.code ?? null : null,
|
|
55
|
+
locationCountry: loc?.country?.code ?? null,
|
|
56
|
+
modeOfTransport: hasVessel ? "VESSEL" : null,
|
|
57
|
+
vesselName: m.vessel?.name ?? null,
|
|
58
|
+
vesselImo: m.vessel?.imo != null ? String(m.vessel.imo) : null,
|
|
59
|
+
voyageNumber: m.voyage ?? null
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function oceanRouteExtra(shipment) {
|
|
63
|
+
const route = shipment.route;
|
|
64
|
+
if (!route) return null;
|
|
65
|
+
const pol = route.port_of_loading;
|
|
66
|
+
const pod = route.port_of_discharge;
|
|
67
|
+
const extra = {};
|
|
68
|
+
if (pol?.location) extra.portOfLoading = { unlocode: pol.location.code ?? null, name: pol.location.name ?? null };
|
|
69
|
+
if (pol?.date_of_loading) extra.dateOfLoading = pol.date_of_loading;
|
|
70
|
+
if (pod?.location) extra.portOfDischarge = { unlocode: pod.location.code ?? null, name: pod.location.name ?? null };
|
|
71
|
+
if (pod?.date_of_discharge) extra.dateOfDischarge = pod.date_of_discharge;
|
|
72
|
+
if (pod?.date_of_discharge_predicted) extra.dateOfDischargePredicted = pod.date_of_discharge_predicted;
|
|
73
|
+
if (route.transit_time != null) extra.transitTimeDays = route.transit_time;
|
|
74
|
+
if (route.transit_percentage != null) extra.transitPercentage = route.transit_percentage;
|
|
75
|
+
if (route.ts_count != null) extra.transshipmentCount = route.ts_count;
|
|
76
|
+
if (route.co2_emission != null) extra.co2Emission = route.co2_emission;
|
|
77
|
+
return Object.keys(extra).length ? { shipsgoRoute: extra } : null;
|
|
78
|
+
}
|
|
79
|
+
function mapOceanShipmentToEvents(shipment) {
|
|
80
|
+
const events = [];
|
|
81
|
+
let vesselName = null;
|
|
82
|
+
let vesselImo = null;
|
|
83
|
+
const containers = shipment.containers ?? [];
|
|
84
|
+
for (const container of containers) {
|
|
85
|
+
const movements = container.movements ?? [];
|
|
86
|
+
for (const m of movements) {
|
|
87
|
+
const event = mapOceanMovement(container, m);
|
|
88
|
+
if (!event) continue;
|
|
89
|
+
events.push(event);
|
|
90
|
+
if (event.vesselName) vesselName = event.vesselName;
|
|
91
|
+
if (event.vesselImo) vesselImo = event.vesselImo;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
events,
|
|
96
|
+
containerNumber: shipment.container_number ?? containers[0]?.number ?? null,
|
|
97
|
+
bookingNumber: shipment.booking_number ?? null,
|
|
98
|
+
bolNumber: null,
|
|
99
|
+
vesselName,
|
|
100
|
+
vesselImo,
|
|
101
|
+
extra: oceanRouteExtra(shipment)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const AIR_EVENT_MAP = {
|
|
105
|
+
RCS: "RCS",
|
|
106
|
+
// received from shipper
|
|
107
|
+
MAN: "MAN",
|
|
108
|
+
// manifested
|
|
109
|
+
DEP: "DEPA",
|
|
110
|
+
// departed
|
|
111
|
+
ARR: "ARRI",
|
|
112
|
+
// arrived
|
|
113
|
+
RCF: "RCF",
|
|
114
|
+
// received from flight
|
|
115
|
+
DLV: "DLVR"
|
|
116
|
+
// delivered
|
|
117
|
+
};
|
|
118
|
+
function airMovementId(awb, m) {
|
|
119
|
+
const tsKey = m.status === "EST" ? "EST" : m.timestamp;
|
|
120
|
+
return `${awb}:${m.event}:${m.status}:${tsKey}`;
|
|
121
|
+
}
|
|
122
|
+
function mapAirMovement(awb, m) {
|
|
123
|
+
const eventCode = AIR_EVENT_MAP[m.event];
|
|
124
|
+
if (!eventCode) return null;
|
|
125
|
+
const ts = new Date(m.timestamp);
|
|
126
|
+
if (Number.isNaN(ts.getTime())) return null;
|
|
127
|
+
const loc = m.location;
|
|
128
|
+
return {
|
|
129
|
+
source: "shipsgo",
|
|
130
|
+
sourceEventId: airMovementId(awb, m),
|
|
131
|
+
// Air movements are all transport-leg events (no container/equipment model).
|
|
132
|
+
eventType: "TRANSPORT",
|
|
133
|
+
eventCode,
|
|
134
|
+
eventClassifierCode: classifier(m.status),
|
|
135
|
+
eventDateTime: ts,
|
|
136
|
+
eventDateTimeOffset: isoOffset(m.timestamp),
|
|
137
|
+
description: `ShipsGo air ${m.event} (${m.status})`,
|
|
138
|
+
rawData: { provider: "shipsgo", mode: "air", ...m },
|
|
139
|
+
// Air locations are IATA airports, not UN/LOCODEs — keep only the name/country.
|
|
140
|
+
locationName: loc?.name ?? loc?.iata ?? null,
|
|
141
|
+
locationUnlocode: null,
|
|
142
|
+
locationCountry: loc?.country?.code ?? null,
|
|
143
|
+
modeOfTransport: m.flight ? "AIR" : null
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function mapAirShipmentToEvents(shipment) {
|
|
147
|
+
const awb = shipment.awb_number ?? shipment.reference ?? "";
|
|
148
|
+
const events = [];
|
|
149
|
+
let flightNumber = null;
|
|
150
|
+
const movements = shipment.movements ?? [];
|
|
151
|
+
for (const m of movements) {
|
|
152
|
+
const event = mapAirMovement(awb, m);
|
|
153
|
+
if (!event) continue;
|
|
154
|
+
events.push(event);
|
|
155
|
+
if (m.flight) flightNumber = m.flight;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
events,
|
|
159
|
+
containerNumber: null,
|
|
160
|
+
bookingNumber: null,
|
|
161
|
+
bolNumber: null,
|
|
162
|
+
awbNumber: shipment.awb_number ?? null,
|
|
163
|
+
flightNumber,
|
|
164
|
+
airlineCode: shipment.airline?.iata ?? null,
|
|
165
|
+
airlineName: shipment.airline?.name ?? null,
|
|
166
|
+
airStatus: shipment.status ?? null,
|
|
167
|
+
extra: airExtra(shipment)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function airExtra(shipment) {
|
|
171
|
+
const extra = {};
|
|
172
|
+
const c = shipment.cargo;
|
|
173
|
+
if (c && (c.pieces != null || c.weight != null || c.volume != null)) {
|
|
174
|
+
extra.cargo = {
|
|
175
|
+
pieces: c.pieces ?? null,
|
|
176
|
+
weight: c.weight ?? null,
|
|
177
|
+
weightUnit: c.weight_unit ?? null,
|
|
178
|
+
volume: c.volume ?? null,
|
|
179
|
+
volumeUnit: c.volume_unit ?? null
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const route = shipment.route;
|
|
183
|
+
if (route) {
|
|
184
|
+
const r = {};
|
|
185
|
+
const orig = route.origin;
|
|
186
|
+
const dest = route.destination;
|
|
187
|
+
if (orig?.location) r.origin = { iata: orig.location.iata ?? null, name: orig.location.name ?? null };
|
|
188
|
+
if (orig?.date_of_dep) r.dateOfDeparture = orig.date_of_dep;
|
|
189
|
+
if (dest?.location) r.destination = { iata: dest.location.iata ?? null, name: dest.location.name ?? null };
|
|
190
|
+
if (dest?.date_of_rcf) r.dateOfArrival = dest.date_of_rcf;
|
|
191
|
+
if (route.transit_time != null) r.transitTimeDays = route.transit_time;
|
|
192
|
+
if (route.transit_percentage != null) r.transitPercentage = route.transit_percentage;
|
|
193
|
+
if (route.ts_count != null) r.transshipmentCount = route.ts_count;
|
|
194
|
+
if (route.co2_emission != null) r.co2Emission = route.co2_emission;
|
|
195
|
+
if (Object.keys(r).length) extra.route = r;
|
|
196
|
+
}
|
|
197
|
+
if (shipment.status_extended != null) extra.statusExtended = shipment.status_extended;
|
|
198
|
+
return Object.keys(extra).length ? { shipsgoAir: extra } : null;
|
|
199
|
+
}
|
|
200
|
+
export {
|
|
201
|
+
mapAirShipmentToEvents,
|
|
202
|
+
mapOceanShipmentToEvents
|
|
203
|
+
};
|
|
204
|
+
//# sourceMappingURL=shipsgo-mapper.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/shipment_tracking/lib/shipsgo-mapper.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Maps ShipsGo shipment responses into the internal `CarrierFetchResult` shape,\n * so the existing ingestion pipeline (Shipment upsert, timestamps, status\n * machine, route inference, unit-sync) consumes ShipsGo exactly like a DCSA\n * carrier adapter.\n *\n * Ocean movement codes are translated onto the DCSA equipment/transport\n * vocabulary the downstream event-mapping already understands\n * (see dcsa-event-mapping.ts), so notifier events resolve unchanged.\n */\nimport type { CarrierFetchResult, CarrierFetchedEvent } from './carrier-adapter'\nimport type {\n TrackingEventType,\n TrackingEventClassifierCode,\n ModeOfTransport,\n} from '../data/entities'\nimport type {\n ShipsGoOceanContainer,\n ShipsGoOceanMovement,\n ShipsGoOceanShipment,\n ShipsGoAirMovement,\n ShipsGoAirShipment,\n} from './shipsgo-client'\n\n// ShipsGo ocean event \u2192 { internal DCSA-style eventType + eventCode }.\n// GTIN/LOAD/DISC/GTOT map 1:1; ARRV\u2192ARRI (DCSA spelling); EMSH/EMRT use the\n// closest DCSA equipment code so the notifier resolves (pick-up / drop-off).\nconst OCEAN_EVENT_MAP: Record<string, { eventType: TrackingEventType; eventCode: string }> = {\n EMSH: { eventType: 'EQUIPMENT', eventCode: 'PICK' }, // empty released to shipper\n GTIN: { eventType: 'EQUIPMENT', eventCode: 'GTIN' },\n LOAD: { eventType: 'EQUIPMENT', eventCode: 'LOAD' },\n DEPA: { eventType: 'TRANSPORT', eventCode: 'DEPA' },\n ARRV: { eventType: 'TRANSPORT', eventCode: 'ARRI' },\n DISC: { eventType: 'EQUIPMENT', eventCode: 'DISC' },\n GTOT: { eventType: 'EQUIPMENT', eventCode: 'GTOT' },\n EMRT: { eventType: 'EQUIPMENT', eventCode: 'DROP' }, // empty returned to depot\n}\n\nfunction classifier(status: 'EST' | 'ACT'): TrackingEventClassifierCode {\n return status === 'ACT' ? 'ACT' : 'EST'\n}\n\n/**\n * Deterministic id for dedup \u2014 ShipsGo movements have no stable identifier.\n * ACT (actual) movements key on their timestamp (immutable once they happen);\n * EST (estimated) movements OMIT the timestamp so a shifting estimate maps to\n * the same row and is refreshed in place (see the persistence loop) rather than\n * inserting a new event every poll.\n */\nfunction movementId(container: string, m: ShipsGoOceanMovement): string {\n const tsKey = m.status === 'EST' ? 'EST' : m.timestamp\n return `${container}:${m.event}:${m.status}:${tsKey}`\n}\n\nfunction isLocode(code: string | null | undefined): boolean {\n return typeof code === 'string' && /^[A-Z]{2}[A-Z0-9]{3}$/.test(code)\n}\n\n/**\n * Extracts the local UTC offset from a ShipsGo ISO-8601 timestamp. ShipsGo dates\n * carry the movement's local (port/airport) offset \u2014 e.g. `+08:00` for a Beijing\n * departure \u2014 so `eventDateTime` (the UTC instant) alone loses the local wall\n * clock the carrier reported. We persist the offset in `eventDateTimeOffset` for\n * local-time display, matching the DCSA adapters. Returns null for UTC (`Z` or\n * `+00:00`), which is already the default.\n */\nfunction isoOffset(ts: string): string | null {\n const match = ts.match(/([+-]\\d{2}:\\d{2})$/)\n if (!match || match[1] === '+00:00') return null\n return match[1]\n}\n\n/**\n * ShipsGo container size (feet, e.g. 40) + type (e.g. HC, GP, RF) \u2192 a readable\n * equipment code (\"40HC\"). Not a true ISO 6346 code, but it's the container-type\n * display slot and ShipsGo doesn't return the ISO code. Null when neither given.\n */\nfunction equipmentCode(container: ShipsGoOceanContainer): string | null {\n const size = container.size != null ? String(container.size) : ''\n const type = container.type ?? ''\n const code = `${size}${type}`.trim()\n return code || null\n}\n\nfunction mapOceanMovement(\n container: ShipsGoOceanContainer,\n m: ShipsGoOceanMovement,\n): CarrierFetchedEvent | null {\n const mapping = OCEAN_EVENT_MAP[m.event]\n if (!mapping) return null\n\n const ts = new Date(m.timestamp)\n if (Number.isNaN(ts.getTime())) return null\n\n const loc = m.location\n const hasVessel = Boolean(m.vessel?.name || m.vessel?.imo)\n\n return {\n source: 'shipsgo',\n sourceEventId: movementId(container.number, m),\n eventType: mapping.eventType,\n eventCode: mapping.eventCode,\n eventClassifierCode: classifier(m.status),\n eventDateTime: ts,\n eventDateTimeOffset: isoOffset(m.timestamp),\n description: `ShipsGo ${m.event} (${m.status})`,\n rawData: { provider: 'shipsgo', ...m },\n equipmentReference: container.number,\n isoEquipmentCode: equipmentCode(container),\n locationName: loc?.name ?? null,\n locationUnlocode: isLocode(loc?.code) ? loc?.code ?? null : null,\n locationCountry: loc?.country?.code ?? null,\n modeOfTransport: hasVessel ? ('VESSEL' as ModeOfTransport) : null,\n vesselName: m.vessel?.name ?? null,\n vesselImo: m.vessel?.imo != null ? String(m.vessel.imo) : null,\n voyageNumber: m.voyage ?? null,\n }\n}\n\n/**\n * ShipsGo shipment-level route summary \u2192 a compact object for `Shipment.extra`.\n * Origin/destination UNLOCODEs are already inferred from movement locations, so\n * this captures the extras: POL/POD names + planned/predicted dates, transit\n * time, transit %, transshipment count and CO2. Returns null when route is absent.\n */\nfunction oceanRouteExtra(shipment: ShipsGoOceanShipment): Record<string, unknown> | null {\n const route = shipment.route\n if (!route) return null\n const pol = route.port_of_loading\n const pod = route.port_of_discharge\n const extra: Record<string, unknown> = {}\n if (pol?.location) extra.portOfLoading = { unlocode: pol.location.code ?? null, name: pol.location.name ?? null }\n if (pol?.date_of_loading) extra.dateOfLoading = pol.date_of_loading\n if (pod?.location) extra.portOfDischarge = { unlocode: pod.location.code ?? null, name: pod.location.name ?? null }\n if (pod?.date_of_discharge) extra.dateOfDischarge = pod.date_of_discharge\n if (pod?.date_of_discharge_predicted) extra.dateOfDischargePredicted = pod.date_of_discharge_predicted\n if (route.transit_time != null) extra.transitTimeDays = route.transit_time\n if (route.transit_percentage != null) extra.transitPercentage = route.transit_percentage\n if (route.ts_count != null) extra.transshipmentCount = route.ts_count\n if (route.co2_emission != null) extra.co2Emission = route.co2_emission\n return Object.keys(extra).length ? { shipsgoRoute: extra } : null\n}\n\n/**\n * Maps a full ShipsGo ocean shipment (with containers + movements) into a\n * `CarrierFetchResult`. Emits one event per movement per container; the\n * downstream pipeline discovers containers from `equipmentReference`.\n */\nexport function mapOceanShipmentToEvents(shipment: ShipsGoOceanShipment): CarrierFetchResult {\n const events: CarrierFetchedEvent[] = []\n let vesselName: string | null = null\n let vesselImo: string | null = null\n\n const containers = shipment.containers ?? []\n for (const container of containers) {\n const movements = container.movements ?? []\n for (const m of movements) {\n const event = mapOceanMovement(container, m)\n if (!event) continue\n events.push(event)\n // Track the most recent vessel seen (movements are chronological).\n if (event.vesselName) vesselName = event.vesselName\n if (event.vesselImo) vesselImo = event.vesselImo\n }\n }\n\n return {\n events,\n containerNumber: shipment.container_number ?? containers[0]?.number ?? null,\n bookingNumber: shipment.booking_number ?? null,\n bolNumber: null,\n vesselName,\n vesselImo,\n extra: oceanRouteExtra(shipment),\n }\n}\n\n// \u2500\u2500\u2500 Air \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// ShipsGo air event \u2192 internal event code. DEP/ARR are translated to the DCSA\n// departure/arrival codes so the shared time-extraction (ATD/ATA) and timeline\n// reuse unchanged; the rest keep their native code (informational only \u2014 air\n// status is derived from ShipsGo's top-level status, see deriveAirShipmentStatus).\nconst AIR_EVENT_MAP: Record<string, string> = {\n RCS: 'RCS', // received from shipper\n MAN: 'MAN', // manifested\n DEP: 'DEPA', // departed\n ARR: 'ARRI', // arrived\n RCF: 'RCF', // received from flight\n DLV: 'DLVR', // delivered\n}\n\n/** Deterministic id for dedup \u2014 air movements have no stable identifier.\n * EST omits the timestamp (refreshed in place); ACT keys on it (immutable). */\nfunction airMovementId(awb: string, m: ShipsGoAirMovement): string {\n const tsKey = m.status === 'EST' ? 'EST' : m.timestamp\n return `${awb}:${m.event}:${m.status}:${tsKey}`\n}\n\nfunction mapAirMovement(awb: string, m: ShipsGoAirMovement): CarrierFetchedEvent | null {\n const eventCode = AIR_EVENT_MAP[m.event]\n if (!eventCode) return null\n\n const ts = new Date(m.timestamp)\n if (Number.isNaN(ts.getTime())) return null\n\n const loc = m.location\n return {\n source: 'shipsgo',\n sourceEventId: airMovementId(awb, m),\n // Air movements are all transport-leg events (no container/equipment model).\n eventType: 'TRANSPORT',\n eventCode,\n eventClassifierCode: classifier(m.status),\n eventDateTime: ts,\n eventDateTimeOffset: isoOffset(m.timestamp),\n description: `ShipsGo air ${m.event} (${m.status})`,\n rawData: { provider: 'shipsgo', mode: 'air', ...m },\n // Air locations are IATA airports, not UN/LOCODEs \u2014 keep only the name/country.\n locationName: loc?.name ?? loc?.iata ?? null,\n locationUnlocode: null,\n locationCountry: loc?.country?.code ?? null,\n modeOfTransport: m.flight ? ('AIR' as ModeOfTransport) : null,\n }\n}\n\n/**\n * Maps a full ShipsGo air shipment into a `CarrierFetchResult`. Emits one event\n * per movement (no container fan-out); the downstream air branch creates a\n * single AWB-keyed Shipment and fills flight/airline + the top-level air status.\n */\nexport function mapAirShipmentToEvents(shipment: ShipsGoAirShipment): CarrierFetchResult {\n const awb = shipment.awb_number ?? shipment.reference ?? ''\n const events: CarrierFetchedEvent[] = []\n let flightNumber: string | null = null\n\n const movements = shipment.movements ?? []\n for (const m of movements) {\n const event = mapAirMovement(awb, m)\n if (!event) continue\n events.push(event)\n // Track the most recent flight seen (movements are chronological).\n if (m.flight) flightNumber = m.flight\n }\n\n return {\n events,\n containerNumber: null,\n bookingNumber: null,\n bolNumber: null,\n awbNumber: shipment.awb_number ?? null,\n flightNumber,\n airlineCode: shipment.airline?.iata ?? null,\n airlineName: shipment.airline?.name ?? null,\n airStatus: shipment.status ?? null,\n extra: airExtra(shipment),\n }\n}\n\n/**\n * ShipsGo air shipment-level extras \u2192 `Shipment.extra`: cargo (pieces/weight/\n * volume) and the pieces-per-status breakdown. Returns null when neither given.\n */\nfunction airExtra(shipment: ShipsGoAirShipment): Record<string, unknown> | null {\n const extra: Record<string, unknown> = {}\n const c = shipment.cargo\n if (c && (c.pieces != null || c.weight != null || c.volume != null)) {\n extra.cargo = {\n pieces: c.pieces ?? null,\n weight: c.weight ?? null,\n weightUnit: c.weight_unit ?? null,\n volume: c.volume ?? null,\n volumeUnit: c.volume_unit ?? null,\n }\n }\n // Air route mirrors the ocean route (see oceanRouteExtra). Air locations are\n // IATA airports (not UN/LOCODEs), so event-based origin/destination inference\n // yields nothing \u2014 this is the only place origin/destination surface for air.\n const route = shipment.route\n if (route) {\n const r: Record<string, unknown> = {}\n const orig = route.origin\n const dest = route.destination\n if (orig?.location) r.origin = { iata: orig.location.iata ?? null, name: orig.location.name ?? null }\n if (orig?.date_of_dep) r.dateOfDeparture = orig.date_of_dep\n if (dest?.location) r.destination = { iata: dest.location.iata ?? null, name: dest.location.name ?? null }\n if (dest?.date_of_rcf) r.dateOfArrival = dest.date_of_rcf\n if (route.transit_time != null) r.transitTimeDays = route.transit_time\n if (route.transit_percentage != null) r.transitPercentage = route.transit_percentage\n if (route.ts_count != null) r.transshipmentCount = route.ts_count\n if (route.co2_emission != null) r.co2Emission = route.co2_emission\n if (Object.keys(r).length) extra.route = r\n }\n if (shipment.status_extended != null) extra.statusExtended = shipment.status_extended\n return Object.keys(extra).length ? { shipsgoAir: extra } : null\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const PRE_ARRIVAL_DAYS_THRESHOLD = 7;
|
|
2
|
+
const STATUS_ORDER = [
|
|
3
|
+
"PENDING",
|
|
4
|
+
"BOOKED",
|
|
5
|
+
"DEPARTED",
|
|
6
|
+
"IN_TRANSIT",
|
|
7
|
+
"PRE_ARRIVAL",
|
|
8
|
+
"ARRIVED",
|
|
9
|
+
"DELIVERED"
|
|
10
|
+
];
|
|
11
|
+
function statusRank(status) {
|
|
12
|
+
return STATUS_ORDER.indexOf(status);
|
|
13
|
+
}
|
|
14
|
+
function isAtDestination(eventLocation, destination) {
|
|
15
|
+
if (!eventLocation || !destination) return false;
|
|
16
|
+
return eventLocation.toUpperCase() === destination.toUpperCase();
|
|
17
|
+
}
|
|
18
|
+
function isAtOrigin(eventLocation, origin) {
|
|
19
|
+
if (!eventLocation || !origin) return false;
|
|
20
|
+
return eventLocation.toUpperCase() === origin.toUpperCase();
|
|
21
|
+
}
|
|
22
|
+
function deriveStatusFromEvent(event, context) {
|
|
23
|
+
const code = event.eventCode.toUpperCase();
|
|
24
|
+
const classifierCode = event.eventClassifierCode;
|
|
25
|
+
if (code === "DEPA") {
|
|
26
|
+
if (classifierCode === "ACT") {
|
|
27
|
+
return "DEPARTED";
|
|
28
|
+
}
|
|
29
|
+
if (classifierCode === "PLN" || classifierCode === "EST") {
|
|
30
|
+
return "BOOKED";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (code === "ARRI" && isAtDestination(event.locationUnlocode, context.destinationUnlocode)) {
|
|
34
|
+
if (classifierCode === "ACT") {
|
|
35
|
+
return "ARRIVED";
|
|
36
|
+
}
|
|
37
|
+
return "IN_TRANSIT";
|
|
38
|
+
}
|
|
39
|
+
if (code === "ARRI" && classifierCode === "ACT") {
|
|
40
|
+
return "IN_TRANSIT";
|
|
41
|
+
}
|
|
42
|
+
if (code === "DISC" && isAtDestination(event.locationUnlocode, context.destinationUnlocode)) {
|
|
43
|
+
if (classifierCode === "ACT") {
|
|
44
|
+
return "ARRIVED";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (code === "LOAD" && isAtOrigin(event.locationUnlocode, context.originUnlocode)) {
|
|
48
|
+
if (classifierCode === "ACT") {
|
|
49
|
+
return "BOOKED";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if ((code === "GTOT" || code === "PICK") && isAtDestination(event.locationUnlocode, context.destinationUnlocode)) {
|
|
53
|
+
if (classifierCode === "ACT") {
|
|
54
|
+
return "DELIVERED";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (code === "GTIN" && isAtDestination(event.locationUnlocode, context.destinationUnlocode)) {
|
|
58
|
+
if (classifierCode === "ACT") {
|
|
59
|
+
return "DELIVERED";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (code === "AVPU" && isAtDestination(event.locationUnlocode, context.destinationUnlocode)) {
|
|
63
|
+
if (classifierCode === "ACT") {
|
|
64
|
+
return "ARRIVED";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (code === "CUSR" && isAtDestination(event.locationUnlocode, context.destinationUnlocode)) {
|
|
68
|
+
if (classifierCode === "ACT") {
|
|
69
|
+
return "ARRIVED";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (code === "DLVR" && classifierCode === "ACT") {
|
|
73
|
+
return "DELIVERED";
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function shouldUpgradeToPreArrival(timeContext) {
|
|
78
|
+
if (!timeContext?.eta || timeContext.ata) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const now = /* @__PURE__ */ new Date();
|
|
82
|
+
const daysUntilEta = (timeContext.eta.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24);
|
|
83
|
+
return daysUntilEta <= PRE_ARRIVAL_DAYS_THRESHOLD && daysUntilEta >= 0;
|
|
84
|
+
}
|
|
85
|
+
function deriveShipmentStatus(events, context, currentStatus = "PENDING", timeContext) {
|
|
86
|
+
let best = currentStatus;
|
|
87
|
+
for (const event of events) {
|
|
88
|
+
const derived = deriveStatusFromEvent(event, context);
|
|
89
|
+
if (derived && statusRank(derived) > statusRank(best)) {
|
|
90
|
+
best = derived;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (statusRank(best) <= statusRank("IN_TRANSIT") && shouldUpgradeToPreArrival(timeContext)) {
|
|
94
|
+
const preArrivalRank = statusRank("PRE_ARRIVAL");
|
|
95
|
+
if (preArrivalRank > statusRank(best)) {
|
|
96
|
+
best = "PRE_ARRIVAL";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return best;
|
|
100
|
+
}
|
|
101
|
+
const AIR_STATUS_MAP = {
|
|
102
|
+
NEW: "PENDING",
|
|
103
|
+
UNTRACKED: "PENDING",
|
|
104
|
+
INPROGRESS: "BOOKED",
|
|
105
|
+
BOOKED: "BOOKED",
|
|
106
|
+
EN_ROUTE: "IN_TRANSIT",
|
|
107
|
+
LANDED: "ARRIVED",
|
|
108
|
+
DELIVERED: "DELIVERED"
|
|
109
|
+
};
|
|
110
|
+
function deriveAirShipmentStatus(airStatus, currentStatus = "PENDING") {
|
|
111
|
+
if (!airStatus) return currentStatus;
|
|
112
|
+
const mapped = AIR_STATUS_MAP[airStatus.toUpperCase()];
|
|
113
|
+
if (!mapped) return currentStatus;
|
|
114
|
+
return statusRank(mapped) > statusRank(currentStatus) ? mapped : currentStatus;
|
|
115
|
+
}
|
|
116
|
+
function evaluatePreArrivalUpgrade(currentStatus, timeContext) {
|
|
117
|
+
if (currentStatus !== "IN_TRANSIT") {
|
|
118
|
+
return currentStatus;
|
|
119
|
+
}
|
|
120
|
+
if (shouldUpgradeToPreArrival(timeContext)) {
|
|
121
|
+
return "PRE_ARRIVAL";
|
|
122
|
+
}
|
|
123
|
+
return currentStatus;
|
|
124
|
+
}
|
|
125
|
+
export {
|
|
126
|
+
PRE_ARRIVAL_DAYS_THRESHOLD,
|
|
127
|
+
deriveAirShipmentStatus,
|
|
128
|
+
deriveShipmentStatus,
|
|
129
|
+
evaluatePreArrivalUpgrade
|
|
130
|
+
};
|
|
131
|
+
//# sourceMappingURL=status-machine.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/shipment_tracking/lib/status-machine.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ShipmentStatusEnum, TrackingEventClassifierCode } from '../data/entities'\n\nexport type ShipmentStatus = ShipmentStatusEnum\n\ntype EventInput = {\n eventCode: string\n eventClassifierCode?: TrackingEventClassifierCode | null\n locationUnlocode?: string | null\n}\n\ntype ShipmentContext = {\n originUnlocode?: string | null\n destinationUnlocode?: string | null\n}\n\ntype TimeContext = {\n eta?: Date | null\n ata?: Date | null\n}\n\n/**\n * Number of days before ETA when a shipment transitions to PRE_ARRIVAL status.\n * Shipment becomes PRE_ARRIVAL when: ETA is within this many days AND no ATA yet.\n */\nexport const PRE_ARRIVAL_DAYS_THRESHOLD = 7\n\nconst STATUS_ORDER: ShipmentStatus[] = [\n 'PENDING',\n 'BOOKED',\n 'DEPARTED',\n 'IN_TRANSIT',\n 'PRE_ARRIVAL',\n 'ARRIVED',\n 'DELIVERED',\n]\n\nfunction statusRank(status: ShipmentStatus): number {\n return STATUS_ORDER.indexOf(status)\n}\n\nfunction isAtDestination(eventLocation: string | null | undefined, destination: string | null | undefined): boolean {\n if (!eventLocation || !destination) return false\n return eventLocation.toUpperCase() === destination.toUpperCase()\n}\n\nfunction isAtOrigin(eventLocation: string | null | undefined, origin: string | null | undefined): boolean {\n if (!eventLocation || !origin) return false\n return eventLocation.toUpperCase() === origin.toUpperCase()\n}\n\nfunction deriveStatusFromEvent(event: EventInput, context: ShipmentContext): ShipmentStatus | null {\n const code = event.eventCode.toUpperCase()\n const classifierCode = event.eventClassifierCode\n\n // DEPA at origin = DEPARTED\n if (code === 'DEPA') {\n if (classifierCode === 'ACT') {\n return 'DEPARTED'\n }\n if (classifierCode === 'PLN' || classifierCode === 'EST') {\n return 'BOOKED'\n }\n }\n\n // ARRI at destination = ARRIVED (actual) or IN_TRANSIT (planned/estimated approaching)\n if (code === 'ARRI' && isAtDestination(event.locationUnlocode, context.destinationUnlocode)) {\n if (classifierCode === 'ACT') {\n return 'ARRIVED'\n }\n return 'IN_TRANSIT'\n }\n\n // ARRI at non-destination with actual = IN_TRANSIT (transshipment)\n if (code === 'ARRI' && classifierCode === 'ACT') {\n return 'IN_TRANSIT'\n }\n\n // DISC (discharge) at destination = ARRIVED\n if (code === 'DISC' && isAtDestination(event.locationUnlocode, context.destinationUnlocode)) {\n if (classifierCode === 'ACT') {\n return 'ARRIVED'\n }\n }\n\n // LOAD at origin = BOOKED (loaded onto vessel)\n if (code === 'LOAD' && isAtOrigin(event.locationUnlocode, context.originUnlocode)) {\n if (classifierCode === 'ACT') {\n return 'BOOKED'\n }\n }\n\n // Gate out at destination = DELIVERED (GTOT = Gate Out Terminal, PICK = Pick-up)\n if ((code === 'GTOT' || code === 'PICK') && isAtDestination(event.locationUnlocode, context.destinationUnlocode)) {\n if (classifierCode === 'ACT') {\n return 'DELIVERED'\n }\n }\n\n // Gate in at destination = DELIVERED (empty return means cargo was delivered)\n if (code === 'GTIN' && isAtDestination(event.locationUnlocode, context.destinationUnlocode)) {\n if (classifierCode === 'ACT') {\n return 'DELIVERED'\n }\n }\n\n // Available for pick-up at destination = ARRIVED\n if (code === 'AVPU' && isAtDestination(event.locationUnlocode, context.destinationUnlocode)) {\n if (classifierCode === 'ACT') {\n return 'ARRIVED'\n }\n }\n\n // Customs released at destination = ARRIVED\n if (code === 'CUSR' && isAtDestination(event.locationUnlocode, context.destinationUnlocode)) {\n if (classifierCode === 'ACT') {\n return 'ARRIVED'\n }\n }\n\n // Delivery event = DELIVERED\n if (code === 'DLVR' && classifierCode === 'ACT') {\n return 'DELIVERED'\n }\n\n return null\n}\n\n/**\n * Checks if a shipment qualifies for PRE_ARRIVAL status based on time.\n * PRE_ARRIVAL = ETA is within threshold days AND no ATA yet.\n */\nfunction shouldUpgradeToPreArrival(timeContext: TimeContext | undefined): boolean {\n if (!timeContext?.eta || timeContext.ata) {\n return false\n }\n\n const now = new Date()\n const daysUntilEta = (timeContext.eta.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)\n\n return daysUntilEta <= PRE_ARRIVAL_DAYS_THRESHOLD && daysUntilEta >= 0\n}\n\n/**\n * Derives the highest-priority shipment status from a set of tracking events.\n * Status only moves forward (never downgrades), based on DCSA event codes.\n *\n * @param events - Array of tracking events to evaluate\n * @param context - Location context (origin/destination UNLOCODEs)\n * @param currentStatus - Current shipment status (defaults to PENDING)\n * @param timeContext - Optional time context for PRE_ARRIVAL evaluation (ETA/ATA)\n */\nexport function deriveShipmentStatus(\n events: EventInput[],\n context: ShipmentContext,\n currentStatus: ShipmentStatus = 'PENDING',\n timeContext?: TimeContext,\n): ShipmentStatus {\n let best = currentStatus\n\n for (const event of events) {\n const derived = deriveStatusFromEvent(event, context)\n if (derived && statusRank(derived) > statusRank(best)) {\n best = derived\n }\n }\n\n // Time-based PRE_ARRIVAL upgrade:\n // If event-derived status is IN_TRANSIT (or lower) and time conditions are met,\n // upgrade to PRE_ARRIVAL\n if (statusRank(best) <= statusRank('IN_TRANSIT') && shouldUpgradeToPreArrival(timeContext)) {\n const preArrivalRank = statusRank('PRE_ARRIVAL')\n if (preArrivalRank > statusRank(best)) {\n best = 'PRE_ARRIVAL'\n }\n }\n\n return best\n}\n\n/**\n * ShipsGo air top-level status \u2192 internal ShipmentStatusEnum.\n * Air has no container/vessel movement model, so status is driven by ShipsGo's\n * aggregate `status` field rather than event-location matching:\n * NEW/UNTRACKED \u2192 PENDING, BOOKED \u2192 BOOKED, EN_ROUTE \u2192 IN_TRANSIT,\n * LANDED \u2192 ARRIVED, DELIVERED \u2192 DELIVERED. (INPROGRESS \u2248 BOOKED.)\n * Monotonic: never downgrades below the current status.\n */\nconst AIR_STATUS_MAP: Record<string, ShipmentStatus> = {\n NEW: 'PENDING',\n UNTRACKED: 'PENDING',\n INPROGRESS: 'BOOKED',\n BOOKED: 'BOOKED',\n EN_ROUTE: 'IN_TRANSIT',\n LANDED: 'ARRIVED',\n DELIVERED: 'DELIVERED',\n}\n\nexport function deriveAirShipmentStatus(\n airStatus: string | null | undefined,\n currentStatus: ShipmentStatus = 'PENDING',\n): ShipmentStatus {\n if (!airStatus) return currentStatus\n const mapped = AIR_STATUS_MAP[airStatus.toUpperCase()]\n if (!mapped) return currentStatus\n return statusRank(mapped) > statusRank(currentStatus) ? mapped : currentStatus\n}\n\n/**\n * Evaluates if a shipment should be upgraded to PRE_ARRIVAL status.\n * Used by the scheduled job to check IN_TRANSIT shipments.\n *\n * @param currentStatus - Current shipment status\n * @param timeContext - Time context with ETA and ATA\n * @returns The new status (PRE_ARRIVAL if conditions met, otherwise currentStatus)\n */\nexport function evaluatePreArrivalUpgrade(\n currentStatus: ShipmentStatus,\n timeContext: TimeContext,\n): ShipmentStatus {\n // Only upgrade from IN_TRANSIT to PRE_ARRIVAL\n if (currentStatus !== 'IN_TRANSIT') {\n return currentStatus\n }\n\n if (shouldUpgradeToPreArrival(timeContext)) {\n return 'PRE_ARRIVAL'\n }\n\n return currentStatus\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function isAtLocation(eventLocation, target) {
|
|
2
|
+
if (!eventLocation || !target) return false;
|
|
3
|
+
return eventLocation.toUpperCase() === target.toUpperCase();
|
|
4
|
+
}
|
|
5
|
+
function extractShipmentTimes(events, context) {
|
|
6
|
+
const result = {};
|
|
7
|
+
const sorted = [...events].sort((eventA, eventB) => {
|
|
8
|
+
const timeA = eventA.eventCreatedDateTime?.getTime() ?? eventA.eventDateTime.getTime();
|
|
9
|
+
const timeB = eventB.eventCreatedDateTime?.getTime() ?? eventB.eventDateTime.getTime();
|
|
10
|
+
return timeA - timeB;
|
|
11
|
+
});
|
|
12
|
+
for (const event of sorted) {
|
|
13
|
+
const code = event.eventCode.toUpperCase();
|
|
14
|
+
const classifierCode = event.eventClassifierCode;
|
|
15
|
+
const atOrigin = isAtLocation(event.locationUnlocode, context.originUnlocode);
|
|
16
|
+
const atDestination = isAtLocation(event.locationUnlocode, context.destinationUnlocode);
|
|
17
|
+
if (code === "DEPA") {
|
|
18
|
+
if (classifierCode === "ACT" && (atOrigin || !context.originUnlocode)) {
|
|
19
|
+
result.atd = event.eventDateTime;
|
|
20
|
+
result.atdOffset = event.eventDateTimeOffset ?? null;
|
|
21
|
+
} else if ((classifierCode === "PLN" || classifierCode === "EST") && (atOrigin || !context.originUnlocode)) {
|
|
22
|
+
result.etd = event.eventDateTime;
|
|
23
|
+
result.etdOffset = event.eventDateTimeOffset ?? null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (code === "ARRI") {
|
|
27
|
+
if (classifierCode === "ACT" && (atDestination || !context.destinationUnlocode)) {
|
|
28
|
+
result.ata = event.eventDateTime;
|
|
29
|
+
result.ataOffset = event.eventDateTimeOffset ?? null;
|
|
30
|
+
} else if ((classifierCode === "PLN" || classifierCode === "EST") && (atDestination || !context.destinationUnlocode)) {
|
|
31
|
+
result.eta = event.eventDateTime;
|
|
32
|
+
result.etaOffset = event.eventDateTimeOffset ?? null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
extractShipmentTimes
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=time-extraction.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/shipment_tracking/lib/time-extraction.ts"],
|
|
4
|
+
"sourcesContent": ["import type { TrackingEventClassifierCode } from '../data/entities'\n\ntype EventInput = {\n eventCode: string\n eventClassifierCode?: TrackingEventClassifierCode | null\n eventDateTime: Date\n eventDateTimeOffset?: string | null\n eventCreatedDateTime?: Date | null\n locationUnlocode?: string | null\n}\n\ntype ShipmentContext = {\n originUnlocode?: string | null\n destinationUnlocode?: string | null\n}\n\nexport type ExtractedTimes = {\n etd?: Date | null\n etdOffset?: string | null\n eta?: Date | null\n etaOffset?: string | null\n atd?: Date | null\n atdOffset?: string | null\n ata?: Date | null\n ataOffset?: string | null\n}\n\nfunction isAtLocation(eventLocation: string | null | undefined, target: string | null | undefined): boolean {\n if (!eventLocation || !target) return false\n return eventLocation.toUpperCase() === target.toUpperCase()\n}\n\n/**\n * Extracts ETD/ETA/ATD/ATA from cargo events.\n *\n * Logic:\n * - DEPA ACT at origin -> ATD\n * - DEPA PLN/EST at origin -> ETD\n * - ARRI ACT at destination -> ATA\n * - ARRI PLN/EST at destination -> ETA\n *\n * When multiple events match the same field, the latest one wins.\n * For EST/PLN events, we use eventCreatedDateTime (when estimate was published)\n * to ensure revised estimates supersede earlier ones.\n */\nexport function extractShipmentTimes(events: EventInput[], context: ShipmentContext): ExtractedTimes {\n const result: ExtractedTimes = {}\n\n // Sort events by createdDateTime (when published) if available, else eventDateTime\n // This ensures revised estimates (published later) supersede earlier ones\n const sorted = [...events].sort((eventA, eventB) => {\n const timeA = eventA.eventCreatedDateTime?.getTime() ?? eventA.eventDateTime.getTime()\n const timeB = eventB.eventCreatedDateTime?.getTime() ?? eventB.eventDateTime.getTime()\n return timeA - timeB\n })\n\n for (const event of sorted) {\n const code = event.eventCode.toUpperCase()\n const classifierCode = event.eventClassifierCode\n const atOrigin = isAtLocation(event.locationUnlocode, context.originUnlocode)\n const atDestination = isAtLocation(event.locationUnlocode, context.destinationUnlocode)\n\n if (code === 'DEPA') {\n if (classifierCode === 'ACT' && (atOrigin || !context.originUnlocode)) {\n result.atd = event.eventDateTime\n result.atdOffset = event.eventDateTimeOffset ?? null\n } else if ((classifierCode === 'PLN' || classifierCode === 'EST') && (atOrigin || !context.originUnlocode)) {\n result.etd = event.eventDateTime\n result.etdOffset = event.eventDateTimeOffset ?? null\n }\n }\n\n if (code === 'ARRI') {\n if (classifierCode === 'ACT' && (atDestination || !context.destinationUnlocode)) {\n result.ata = event.eventDateTime\n result.ataOffset = event.eventDateTimeOffset ?? null\n } else if ((classifierCode === 'PLN' || classifierCode === 'EST') && (atDestination || !context.destinationUnlocode)) {\n result.eta = event.eventDateTime\n result.etaOffset = event.eventDateTimeOffset ?? null\n }\n }\n }\n\n return result\n}\n"],
|
|
5
|
+
"mappings": "AA2BA,SAAS,aAAa,eAA0C,QAA4C;AAC1G,MAAI,CAAC,iBAAiB,CAAC,OAAQ,QAAO;AACtC,SAAO,cAAc,YAAY,MAAM,OAAO,YAAY;AAC5D;AAeO,SAAS,qBAAqB,QAAsB,SAA0C;AACnG,QAAM,SAAyB,CAAC;AAIhC,QAAM,SAAS,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,QAAQ,WAAW;AAClD,UAAM,QAAQ,OAAO,sBAAsB,QAAQ,KAAK,OAAO,cAAc,QAAQ;AACrF,UAAM,QAAQ,OAAO,sBAAsB,QAAQ,KAAK,OAAO,cAAc,QAAQ;AACrF,WAAO,QAAQ;AAAA,EACjB,CAAC;AAED,aAAW,SAAS,QAAQ;AAC1B,UAAM,OAAO,MAAM,UAAU,YAAY;AACzC,UAAM,iBAAiB,MAAM;AAC7B,UAAM,WAAW,aAAa,MAAM,kBAAkB,QAAQ,cAAc;AAC5E,UAAM,gBAAgB,aAAa,MAAM,kBAAkB,QAAQ,mBAAmB;AAEtF,QAAI,SAAS,QAAQ;AACnB,UAAI,mBAAmB,UAAU,YAAY,CAAC,QAAQ,iBAAiB;AACrE,eAAO,MAAM,MAAM;AACnB,eAAO,YAAY,MAAM,uBAAuB;AAAA,MAClD,YAAY,mBAAmB,SAAS,mBAAmB,WAAW,YAAY,CAAC,QAAQ,iBAAiB;AAC1G,eAAO,MAAM,MAAM;AACnB,eAAO,YAAY,MAAM,uBAAuB;AAAA,MAClD;AAAA,IACF;AAEA,QAAI,SAAS,QAAQ;AACnB,UAAI,mBAAmB,UAAU,iBAAiB,CAAC,QAAQ,sBAAsB;AAC/E,eAAO,MAAM,MAAM;AACnB,eAAO,YAAY,MAAM,uBAAuB;AAAA,MAClD,YAAY,mBAAmB,SAAS,mBAAmB,WAAW,iBAAiB,CAAC,QAAQ,sBAAsB;AACpH,eAAO,MAAM,MAAM;AACnB,eAAO,YAAY,MAAM,uBAAuB;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|