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