@freighttech/terminal-tracking 0.7.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 (236) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -0
  3. package/dist/generated/entities/terminal_config/index.js +53 -0
  4. package/dist/generated/entities/terminal_config/index.js.map +7 -0
  5. package/dist/generated/entities/terminal_event/index.js +59 -0
  6. package/dist/generated/entities/terminal_event/index.js.map +7 -0
  7. package/dist/generated/entities/terminal_tracking_job/index.js +37 -0
  8. package/dist/generated/entities/terminal_tracking_job/index.js.map +7 -0
  9. package/dist/generated/entities/terminal_vessel_visit/index.js +41 -0
  10. package/dist/generated/entities/terminal_vessel_visit/index.js.map +7 -0
  11. package/dist/generated/entities.ids.generated.js +26 -0
  12. package/dist/generated/entities.ids.generated.js.map +7 -0
  13. package/dist/index.js +20 -0
  14. package/dist/index.js.map +7 -0
  15. package/dist/modules/terminal_tracking/acl.js +14 -0
  16. package/dist/modules/terminal_tracking/acl.js.map +7 -0
  17. package/dist/modules/terminal_tracking/api/openapi.js +27 -0
  18. package/dist/modules/terminal_tracking/api/openapi.js.map +7 -0
  19. package/dist/modules/terminal_tracking/api/terminal-configs/route.js +140 -0
  20. package/dist/modules/terminal_tracking/api/terminal-configs/route.js.map +7 -0
  21. package/dist/modules/terminal_tracking/api/terminal-configs/test/route.js +58 -0
  22. package/dist/modules/terminal_tracking/api/terminal-configs/test/route.js.map +7 -0
  23. package/dist/modules/terminal_tracking/api/tracking-jobs/details/route.js +109 -0
  24. package/dist/modules/terminal_tracking/api/tracking-jobs/details/route.js.map +7 -0
  25. package/dist/modules/terminal_tracking/api/tracking-jobs/poll/route.js +58 -0
  26. package/dist/modules/terminal_tracking/api/tracking-jobs/poll/route.js.map +7 -0
  27. package/dist/modules/terminal_tracking/api/tracking-jobs/route.js +189 -0
  28. package/dist/modules/terminal_tracking/api/tracking-jobs/route.js.map +7 -0
  29. package/dist/modules/terminal_tracking/api/utils.js +12 -0
  30. package/dist/modules/terminal_tracking/api/utils.js.map +7 -0
  31. package/dist/modules/terminal_tracking/api/vessel-visits/route.js +83 -0
  32. package/dist/modules/terminal_tracking/api/vessel-visits/route.js.map +7 -0
  33. package/dist/modules/terminal_tracking/backend/terminal-configs/page.js +669 -0
  34. package/dist/modules/terminal_tracking/backend/terminal-configs/page.js.map +7 -0
  35. package/dist/modules/terminal_tracking/backend/terminal-configs/page.meta.js +19 -0
  36. package/dist/modules/terminal_tracking/backend/terminal-configs/page.meta.js.map +7 -0
  37. package/dist/modules/terminal_tracking/backend/terminal-containers/page.js +275 -0
  38. package/dist/modules/terminal_tracking/backend/terminal-containers/page.js.map +7 -0
  39. package/dist/modules/terminal_tracking/backend/terminal-containers/page.meta.js +19 -0
  40. package/dist/modules/terminal_tracking/backend/terminal-containers/page.meta.js.map +7 -0
  41. package/dist/modules/terminal_tracking/ce.js +35 -0
  42. package/dist/modules/terminal_tracking/ce.js.map +7 -0
  43. package/dist/modules/terminal_tracking/commands/index.js +4 -0
  44. package/dist/modules/terminal_tracking/commands/index.js.map +7 -0
  45. package/dist/modules/terminal_tracking/commands/terminal-configs.js +107 -0
  46. package/dist/modules/terminal_tracking/commands/terminal-configs.js.map +7 -0
  47. package/dist/modules/terminal_tracking/commands/terminal-tracking.js +27 -0
  48. package/dist/modules/terminal_tracking/commands/terminal-tracking.js.map +7 -0
  49. package/dist/modules/terminal_tracking/commands/tracking-jobs.js +56 -0
  50. package/dist/modules/terminal_tracking/commands/tracking-jobs.js.map +7 -0
  51. package/dist/modules/terminal_tracking/components/ContainerDetailsDrawer.js +371 -0
  52. package/dist/modules/terminal_tracking/components/ContainerDetailsDrawer.js.map +7 -0
  53. package/dist/modules/terminal_tracking/data/entities.js +337 -0
  54. package/dist/modules/terminal_tracking/data/entities.js.map +7 -0
  55. package/dist/modules/terminal_tracking/data/validators.js +98 -0
  56. package/dist/modules/terminal_tracking/data/validators.js.map +7 -0
  57. package/dist/modules/terminal_tracking/di.js +39 -0
  58. package/dist/modules/terminal_tracking/di.js.map +7 -0
  59. package/dist/modules/terminal_tracking/encryption.js +14 -0
  60. package/dist/modules/terminal_tracking/encryption.js.map +7 -0
  61. package/dist/modules/terminal_tracking/events.js +45 -0
  62. package/dist/modules/terminal_tracking/events.js.map +7 -0
  63. package/dist/modules/terminal_tracking/i18n/de.json +175 -0
  64. package/dist/modules/terminal_tracking/i18n/en.json +178 -0
  65. package/dist/modules/terminal_tracking/i18n/es.json +175 -0
  66. package/dist/modules/terminal_tracking/i18n/pl.json +206 -0
  67. package/dist/modules/terminal_tracking/index.js +15 -0
  68. package/dist/modules/terminal_tracking/index.js.map +7 -0
  69. package/dist/modules/terminal_tracking/lib/adapters/bct/adapter.js +164 -0
  70. package/dist/modules/terminal_tracking/lib/adapters/bct/adapter.js.map +7 -0
  71. package/dist/modules/terminal_tracking/lib/adapters/bct/auth/basic.js +23 -0
  72. package/dist/modules/terminal_tracking/lib/adapters/bct/auth/basic.js.map +7 -0
  73. package/dist/modules/terminal_tracking/lib/adapters/bct/incos-semantics.js +146 -0
  74. package/dist/modules/terminal_tracking/lib/adapters/bct/incos-semantics.js.map +7 -0
  75. package/dist/modules/terminal_tracking/lib/adapters/bct/types.js +1 -0
  76. package/dist/modules/terminal_tracking/lib/adapters/bct/types.js.map +7 -0
  77. package/dist/modules/terminal_tracking/lib/adapters/gct/adapter.js +106 -0
  78. package/dist/modules/terminal_tracking/lib/adapters/gct/adapter.js.map +7 -0
  79. package/dist/modules/terminal_tracking/lib/adapters/gct/auth/token.js +120 -0
  80. package/dist/modules/terminal_tracking/lib/adapters/gct/auth/token.js.map +7 -0
  81. package/dist/modules/terminal_tracking/lib/adapters/gct/gct-semantics.js +77 -0
  82. package/dist/modules/terminal_tracking/lib/adapters/gct/gct-semantics.js.map +7 -0
  83. package/dist/modules/terminal_tracking/lib/adapters/gct/types.js +1 -0
  84. package/dist/modules/terminal_tracking/lib/adapters/gct/types.js.map +7 -0
  85. package/dist/modules/terminal_tracking/lib/adapters/index.js +12 -0
  86. package/dist/modules/terminal_tracking/lib/adapters/index.js.map +7 -0
  87. package/dist/modules/terminal_tracking/lib/adapters/n4/adapter.js +121 -0
  88. package/dist/modules/terminal_tracking/lib/adapters/n4/adapter.js.map +7 -0
  89. package/dist/modules/terminal_tracking/lib/adapters/n4/auth/ropc.js +81 -0
  90. package/dist/modules/terminal_tracking/lib/adapters/n4/auth/ropc.js.map +7 -0
  91. package/dist/modules/terminal_tracking/lib/adapters/n4/http.js +66 -0
  92. package/dist/modules/terminal_tracking/lib/adapters/n4/http.js.map +7 -0
  93. package/dist/modules/terminal_tracking/lib/adapters/n4/n4-semantics.js +95 -0
  94. package/dist/modules/terminal_tracking/lib/adapters/n4/n4-semantics.js.map +7 -0
  95. package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.js +70 -0
  96. package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.js.map +7 -0
  97. package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.js +44 -0
  98. package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.js.map +7 -0
  99. package/dist/modules/terminal_tracking/lib/adapters/n4/proxy.js +15 -0
  100. package/dist/modules/terminal_tracking/lib/adapters/n4/proxy.js.map +7 -0
  101. package/dist/modules/terminal_tracking/lib/adapters/n4/types.js +1 -0
  102. package/dist/modules/terminal_tracking/lib/adapters/n4/types.js.map +7 -0
  103. package/dist/modules/terminal_tracking/lib/adapters/zoned-time.js +27 -0
  104. package/dist/modules/terminal_tracking/lib/adapters/zoned-time.js.map +7 -0
  105. package/dist/modules/terminal_tracking/lib/availability.js +40 -0
  106. package/dist/modules/terminal_tracking/lib/availability.js.map +7 -0
  107. package/dist/modules/terminal_tracking/lib/ensure-schedule.js +51 -0
  108. package/dist/modules/terminal_tracking/lib/ensure-schedule.js.map +7 -0
  109. package/dist/modules/terminal_tracking/lib/holds.js +240 -0
  110. package/dist/modules/terminal_tracking/lib/holds.js.map +7 -0
  111. package/dist/modules/terminal_tracking/lib/logger.js +19 -0
  112. package/dist/modules/terminal_tracking/lib/logger.js.map +7 -0
  113. package/dist/modules/terminal_tracking/lib/rate-limiter.js +30 -0
  114. package/dist/modules/terminal_tracking/lib/rate-limiter.js.map +7 -0
  115. package/dist/modules/terminal_tracking/lib/stops.js +48 -0
  116. package/dist/modules/terminal_tracking/lib/stops.js.map +7 -0
  117. package/dist/modules/terminal_tracking/lib/terminal-adapter.js +1 -0
  118. package/dist/modules/terminal_tracking/lib/terminal-adapter.js.map +7 -0
  119. package/dist/modules/terminal_tracking/lib/totp.js +41 -0
  120. package/dist/modules/terminal_tracking/lib/totp.js.map +7 -0
  121. package/dist/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.js +101 -0
  122. package/dist/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.js.map +7 -0
  123. package/dist/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.js +39 -0
  124. package/dist/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.js.map +7 -0
  125. package/dist/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.js +13 -0
  126. package/dist/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.js.map +7 -0
  127. package/dist/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.js +15 -0
  128. package/dist/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.js.map +7 -0
  129. package/dist/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.js +13 -0
  130. package/dist/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.js.map +7 -0
  131. package/dist/modules/terminal_tracking/services/terminalMatcherService.js +61 -0
  132. package/dist/modules/terminal_tracking/services/terminalMatcherService.js.map +7 -0
  133. package/dist/modules/terminal_tracking/services/terminalRegistry.js +24 -0
  134. package/dist/modules/terminal_tracking/services/terminalRegistry.js.map +7 -0
  135. package/dist/modules/terminal_tracking/services/terminalTrackingService.js +687 -0
  136. package/dist/modules/terminal_tracking/services/terminalTrackingService.js.map +7 -0
  137. package/dist/modules/terminal_tracking/setup.js +23 -0
  138. package/dist/modules/terminal_tracking/setup.js.map +7 -0
  139. package/dist/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.js +22 -0
  140. package/dist/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.js.map +7 -0
  141. package/generated/entities/terminal_config/index.ts +36 -0
  142. package/generated/entities/terminal_event/index.ts +39 -0
  143. package/generated/entities/terminal_tracking_job/index.ts +28 -0
  144. package/generated/entities/terminal_vessel_visit/index.ts +30 -0
  145. package/generated/entities.ids.generated.ts +52 -0
  146. package/package.json +80 -0
  147. package/src/index.ts +44 -0
  148. package/src/modules/terminal_tracking/__integration__/TC-TRACK-301-gct-config-test-connection.spec.ts +66 -0
  149. package/src/modules/terminal_tracking/__integration__/TC-TRACK-317-bct-config-test-connection.spec.ts +68 -0
  150. package/src/modules/terminal_tracking/__integration__/helpers.ts +148 -0
  151. package/src/modules/terminal_tracking/__integration__/meta.ts +4 -0
  152. package/src/modules/terminal_tracking/acl.ts +10 -0
  153. package/src/modules/terminal_tracking/api/openapi.ts +28 -0
  154. package/src/modules/terminal_tracking/api/terminal-configs/route.ts +140 -0
  155. package/src/modules/terminal_tracking/api/terminal-configs/test/route.ts +64 -0
  156. package/src/modules/terminal_tracking/api/tracking-jobs/details/route.ts +116 -0
  157. package/src/modules/terminal_tracking/api/tracking-jobs/poll/route.ts +64 -0
  158. package/src/modules/terminal_tracking/api/tracking-jobs/route.ts +243 -0
  159. package/src/modules/terminal_tracking/api/utils.ts +10 -0
  160. package/src/modules/terminal_tracking/api/vessel-visits/route.ts +95 -0
  161. package/src/modules/terminal_tracking/backend/terminal-configs/page.meta.ts +16 -0
  162. package/src/modules/terminal_tracking/backend/terminal-configs/page.tsx +728 -0
  163. package/src/modules/terminal_tracking/backend/terminal-containers/page.meta.ts +16 -0
  164. package/src/modules/terminal_tracking/backend/terminal-containers/page.tsx +328 -0
  165. package/src/modules/terminal_tracking/ce.ts +31 -0
  166. package/src/modules/terminal_tracking/commands/index.ts +3 -0
  167. package/src/modules/terminal_tracking/commands/terminal-configs.ts +132 -0
  168. package/src/modules/terminal_tracking/commands/terminal-tracking.ts +34 -0
  169. package/src/modules/terminal_tracking/commands/tracking-jobs.ts +69 -0
  170. package/src/modules/terminal_tracking/components/ContainerDetailsDrawer.tsx +540 -0
  171. package/src/modules/terminal_tracking/data/entities.ts +386 -0
  172. package/src/modules/terminal_tracking/data/validators.ts +113 -0
  173. package/src/modules/terminal_tracking/di.ts +46 -0
  174. package/src/modules/terminal_tracking/encryption.ts +12 -0
  175. package/src/modules/terminal_tracking/events.ts +48 -0
  176. package/src/modules/terminal_tracking/i18n/de.json +175 -0
  177. package/src/modules/terminal_tracking/i18n/en.json +178 -0
  178. package/src/modules/terminal_tracking/i18n/es.json +175 -0
  179. package/src/modules/terminal_tracking/i18n/pl.json +206 -0
  180. package/src/modules/terminal_tracking/index.ts +13 -0
  181. package/src/modules/terminal_tracking/lib/__tests__/availability.test.ts +136 -0
  182. package/src/modules/terminal_tracking/lib/__tests__/ensure-schedule.test.ts +67 -0
  183. package/src/modules/terminal_tracking/lib/__tests__/holds.test.ts +73 -0
  184. package/src/modules/terminal_tracking/lib/__tests__/stops.test.ts +80 -0
  185. package/src/modules/terminal_tracking/lib/__tests__/totp.test.ts +38 -0
  186. package/src/modules/terminal_tracking/lib/adapters/bct/__tests__/adapter.test.ts +196 -0
  187. package/src/modules/terminal_tracking/lib/adapters/bct/__tests__/incos-semantics.test.ts +303 -0
  188. package/src/modules/terminal_tracking/lib/adapters/bct/adapter.ts +219 -0
  189. package/src/modules/terminal_tracking/lib/adapters/bct/auth/basic.ts +34 -0
  190. package/src/modules/terminal_tracking/lib/adapters/bct/incos-semantics.ts +291 -0
  191. package/src/modules/terminal_tracking/lib/adapters/bct/types.ts +113 -0
  192. package/src/modules/terminal_tracking/lib/adapters/gct/__tests__/adapter.test.ts +238 -0
  193. package/src/modules/terminal_tracking/lib/adapters/gct/__tests__/gct-semantics.test.ts +133 -0
  194. package/src/modules/terminal_tracking/lib/adapters/gct/adapter.ts +152 -0
  195. package/src/modules/terminal_tracking/lib/adapters/gct/auth/token.ts +219 -0
  196. package/src/modules/terminal_tracking/lib/adapters/gct/gct-semantics.ts +136 -0
  197. package/src/modules/terminal_tracking/lib/adapters/gct/types.ts +58 -0
  198. package/src/modules/terminal_tracking/lib/adapters/index.ts +16 -0
  199. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/adapter-proxy.test.ts +69 -0
  200. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/data-table.test.ts +179 -0
  201. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/http.test.ts +98 -0
  202. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/n4-semantics.test.ts +140 -0
  203. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/proxy.test.ts +19 -0
  204. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/vessel-data-table.test.ts +67 -0
  205. package/src/modules/terminal_tracking/lib/adapters/n4/adapter.ts +163 -0
  206. package/src/modules/terminal_tracking/lib/adapters/n4/auth/ropc.ts +104 -0
  207. package/src/modules/terminal_tracking/lib/adapters/n4/http.ts +127 -0
  208. package/src/modules/terminal_tracking/lib/adapters/n4/n4-semantics.ts +156 -0
  209. package/src/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.ts +102 -0
  210. package/src/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.ts +60 -0
  211. package/src/modules/terminal_tracking/lib/adapters/n4/proxy.ts +24 -0
  212. package/src/modules/terminal_tracking/lib/adapters/n4/types.ts +56 -0
  213. package/src/modules/terminal_tracking/lib/adapters/zoned-time.ts +65 -0
  214. package/src/modules/terminal_tracking/lib/availability.ts +119 -0
  215. package/src/modules/terminal_tracking/lib/ensure-schedule.ts +83 -0
  216. package/src/modules/terminal_tracking/lib/holds.ts +344 -0
  217. package/src/modules/terminal_tracking/lib/logger.ts +21 -0
  218. package/src/modules/terminal_tracking/lib/rate-limiter.ts +52 -0
  219. package/src/modules/terminal_tracking/lib/stops.ts +124 -0
  220. package/src/modules/terminal_tracking/lib/terminal-adapter.ts +136 -0
  221. package/src/modules/terminal_tracking/lib/totp.ts +63 -0
  222. package/src/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.ts +104 -0
  223. package/src/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.ts +40 -0
  224. package/src/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.ts +11 -0
  225. package/src/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.ts +13 -0
  226. package/src/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.ts +16 -0
  227. package/src/modules/terminal_tracking/services/__tests__/availabilityEmit.test.ts +277 -0
  228. package/src/modules/terminal_tracking/services/__tests__/createJob.test.ts +59 -0
  229. package/src/modules/terminal_tracking/services/__tests__/pollAllBatch.test.ts +147 -0
  230. package/src/modules/terminal_tracking/services/__tests__/terminalMatcher.test.ts +70 -0
  231. package/src/modules/terminal_tracking/services/__tests__/vesselResolution.test.ts +453 -0
  232. package/src/modules/terminal_tracking/services/terminalMatcherService.ts +97 -0
  233. package/src/modules/terminal_tracking/services/terminalRegistry.ts +30 -0
  234. package/src/modules/terminal_tracking/services/terminalTrackingService.ts +912 -0
  235. package/src/modules/terminal_tracking/setup.ts +24 -0
  236. package/src/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.ts +32 -0
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Load/pickup "STOP" flags for Navis N4 terminals (Baltic Hub / BCT).
3
+ *
4
+ * The N4 `/unit` endpoint gained three columns (Baltic Hub API update, Aug 2026)
5
+ * that state, per transport mode, whether the container currently CANNOT be
6
+ * loaded / picked up:
7
+ *
8
+ * - **Stop-Vsl** — no loading onto a **vessel** (export blocked).
9
+ * - **Stop-Road** — no pickup by **road / truck**.
10
+ * - **Stop-Rail** — no pickup by **rail**.
11
+ *
12
+ * Unlike the `Unit Impediments` hold list (from which blocking is *inferred* via
13
+ * the catalogue in `./holds`), these are the terminal stating the restriction
14
+ * directly, so they are a cleaner mode-specific signal.
15
+ *
16
+ * Pure helper — no DI, no i18n, no React. Reads from the N4 row's `rawData` map
17
+ * (header → value) so the SAME parse serves the `/unit` parser, the availability
18
+ * logic (which runs on the persisted event's `rawData`), and the details drawer.
19
+ *
20
+ * ── Header + value format ───────────────────────────────────────────────────
21
+ * Confirmed against a real Baltic Hub `/unit` payload (2026-07): headers are
22
+ * `Stop-Vsl` / `Stop-Rail` / `Stop-Road`, values are the strings `"true"` /
23
+ * `"false"`. Header matching is still done **tolerantly** (case- and
24
+ * punctuation-insensitive) and value parsing treats any non-empty, non-falsy
25
+ * token as an active stop, so a minor terminal-side format change (or another
26
+ * N4 terminal spelling the columns differently) keeps working — everything
27
+ * keys off this one file.
28
+ */
29
+
30
+ /** Transport mode a STOP flag applies to. */
31
+ export type TerminalStopMode = 'vsl' | 'road' | 'rail'
32
+
33
+ /**
34
+ * The three STOP flags for a container. `null` means the terminal did not report
35
+ * that column at all (older terminals / pre-update payloads) — distinct from
36
+ * `false`, which means the column was present and no restriction is active.
37
+ */
38
+ export type TerminalStops = {
39
+ /** No loading onto a vessel (export). */
40
+ vsl: boolean | null
41
+ /** No pickup by road / truck. */
42
+ road: boolean | null
43
+ /** No pickup by rail. */
44
+ rail: boolean | null
45
+ }
46
+
47
+ /** All-unknown stops (nothing reported). */
48
+ export const NO_STOPS: TerminalStops = { vsl: null, road: null, rail: null }
49
+
50
+ /**
51
+ * Candidate column headers per mode, in normalized form (lower-case,
52
+ * non-alphanumerics stripped). First match wins. `stopvessel` is kept alongside
53
+ * `stopvsl` in case the header spells the word out.
54
+ */
55
+ const STOP_HEADERS: Record<TerminalStopMode, readonly string[]> = {
56
+ vsl: ['stopvsl', 'stopvessel'],
57
+ road: ['stoproad'],
58
+ rail: ['stoprail'],
59
+ }
60
+
61
+ /** Values that mean "no stop" when the column is present. */
62
+ const FALSY_TOKENS = new Set(['', '0', 'n', 'no', 'false', 'f', '-'])
63
+
64
+ /** Normalize a header for tolerant matching: lower-case, drop non-alphanumerics. */
65
+ function normalizeHeader(header: string): string {
66
+ return header.toLowerCase().replace(/[^a-z0-9]/g, '')
67
+ }
68
+
69
+ /** Interpret a present column value as an active stop (true) or not (false). */
70
+ function isActiveStop(value: unknown): boolean {
71
+ if (value == null) return false
72
+ return !FALSY_TOKENS.has(String(value).trim().toLowerCase())
73
+ }
74
+
75
+ /**
76
+ * Parse the STOP flags out of an N4 row's `rawData` (header → value). Returns
77
+ * `null` for a mode whose column is absent, `false`/`true` when present.
78
+ */
79
+ export function parseTerminalStops(rawData: Record<string, unknown> | null | undefined): TerminalStops {
80
+ if (!rawData) return { ...NO_STOPS }
81
+
82
+ const byNorm = new Map<string, unknown>()
83
+ for (const [key, value] of Object.entries(rawData)) byNorm.set(normalizeHeader(key), value)
84
+
85
+ const read = (mode: TerminalStopMode): boolean | null => {
86
+ for (const candidate of STOP_HEADERS[mode]) {
87
+ if (byNorm.has(candidate)) return isActiveStop(byNorm.get(candidate))
88
+ }
89
+ return null
90
+ }
91
+
92
+ return { vsl: read('vsl'), road: read('road'), rail: read('rail') }
93
+ }
94
+
95
+ /** True only when road pickup is explicitly stopped (not on null/false). */
96
+ export function isRoadStopped(stops: TerminalStops): boolean {
97
+ return stops.road === true
98
+ }
99
+
100
+ /**
101
+ * Whether the set of *actively-stopped* modes differs between two snapshots.
102
+ * Drives the `equipment.stops_updated` event. Compares on truthiness, so
103
+ * `null` (unreported) and `false` (reported, not stopped) are equivalent —
104
+ * both mean "not stopped" — mirroring how `impedimentsChanged` treats a null
105
+ * and an empty hold list as equal. This keeps a first poll of containers that
106
+ * merely carry all-`false` STOP columns from emitting spurious change events.
107
+ */
108
+ export function stopsChanged(prior: TerminalStops, next: TerminalStops): boolean {
109
+ const active = (v: boolean | null) => v === true
110
+ return (
111
+ active(prior.vsl) !== active(next.vsl) ||
112
+ active(prior.road) !== active(next.road) ||
113
+ active(prior.rail) !== active(next.rail)
114
+ )
115
+ }
116
+
117
+ /** The modes with an active stop, in vsl → road → rail order. */
118
+ export function activeStopModes(stops: TerminalStops): TerminalStopMode[] {
119
+ const modes: TerminalStopMode[] = []
120
+ if (stops.vsl === true) modes.push('vsl')
121
+ if (stops.road === true) modes.push('road')
122
+ if (stops.rail === true) modes.push('rail')
123
+ return modes
124
+ }
@@ -0,0 +1,136 @@
1
+ import type {
2
+ TerminalEventType,
3
+ TerminalEventClassifierCode,
4
+ TerminalEventSource,
5
+ FacilityCodeProvider,
6
+ TerminalModeOfTransport,
7
+ TerminalAuthType,
8
+ TerminalEndpoints,
9
+ TerminalSealInfo,
10
+ } from '../data/entities'
11
+
12
+ /**
13
+ * A terminal config with its `auth_config` already decrypted, handed to
14
+ * adapters so they never touch the ORM or encryption layer directly.
15
+ */
16
+ export type ResolvedTerminalConfig = {
17
+ terminalCode: string
18
+ adapterType: string
19
+ displayName: string
20
+ baseUrl: string
21
+ /** Optional HTTP(S) forward proxy for this terminal's API calls. */
22
+ proxyUrl?: string | null
23
+ endpoints: TerminalEndpoints
24
+ authType: TerminalAuthType
25
+ tokenUrl?: string | null
26
+ scope?: string | null
27
+ clientId?: string | null
28
+ /** Decrypted secrets, e.g. { username, password } or { clientSecret } */
29
+ authConfig?: Record<string, unknown> | null
30
+ rateLimitRequests: number
31
+ rateLimitWindowSeconds: number
32
+ unlocode?: string | null
33
+ facilityCode?: string | null
34
+ facilityCodeListProvider?: FacilityCodeProvider | null
35
+ }
36
+
37
+ /** A normalized, terminal-sourced event ready to persist as a TerminalEvent. */
38
+ export type TerminalFetchedEvent = {
39
+ source: TerminalEventSource
40
+ sourceEventId: string // '{terminalCode}:{ufvGkey}:{eventCode}'
41
+ eventType: TerminalEventType
42
+ eventCode: string
43
+ eventClassifierCode?: TerminalEventClassifierCode | null
44
+ eventDateTime: Date
45
+ containerNumber: string
46
+ ufvGkey: string
47
+ transitState?: string | null
48
+ visitState?: string | null
49
+ facilityCode?: string | null
50
+ facilityCodeListProvider?: FacilityCodeProvider | null
51
+ unlocode?: string | null
52
+ visitRefIn?: string | null
53
+ visitRefOut?: string | null
54
+ vesselName?: string | null
55
+ voyageNumber?: string | null
56
+ modeOfTransport?: TerminalModeOfTransport | null
57
+ seals?: TerminalSealInfo[] | null
58
+ vgmWeightKg?: number | null
59
+ impediments?: string[] | null
60
+ // When the container was loaded onto its transport (N4 `Loaded` column):
61
+ // export = onto the vessel, import = onto road/rail. Parsed regardless of the
62
+ // container's current transit state, so it survives past S60_LOADED.
63
+ loadedAt?: Date | null
64
+ // Nested vessel visit enrichment (from /VESSEL); populated by the service layer.
65
+ vesselVisit?: NormalizedVesselVisit | null
66
+ rawData?: Record<string, unknown> | null
67
+ }
68
+
69
+ /**
70
+ * A normalized N4 `/VESSEL` visit, keyed by visit ref. Carries the full vessel
71
+ * row: name, both voyages, line, phase, all four timestamps, plus the
72
+ * begin-receive / dry-cutoff window dates.
73
+ */
74
+ export type NormalizedVesselVisit = {
75
+ visitRef: string
76
+ vesselName: string | null
77
+ ibVoyage: string | null
78
+ obVoyage: string | null
79
+ line: string | null
80
+ phase: string | null
81
+ eta: Date | null
82
+ etd: Date | null
83
+ ata: Date | null
84
+ atd: Date | null
85
+ beginReceive: Date | null
86
+ dryCutoff: Date | null
87
+ rawData: Record<string, unknown> | null
88
+ }
89
+
90
+ export type TerminalFetchResult = {
91
+ events: TerminalFetchedEvent[]
92
+ containerNumber: string
93
+ }
94
+
95
+ export type TerminalAdapterTestResult = {
96
+ success: boolean
97
+ message: string
98
+ latencyMs?: number
99
+ }
100
+
101
+ export interface TerminalAdapter {
102
+ /** Selects this adapter from the registry, e.g. 'n4'. */
103
+ readonly adapterType: string
104
+
105
+ /** Standard endpoint paths for this adapter family; used when a terminal config omits them. */
106
+ readonly defaultEndpoints: TerminalEndpoints
107
+
108
+ fetchEvents(input: {
109
+ containerNumber: string
110
+ config: ResolvedTerminalConfig
111
+ }): Promise<TerminalFetchResult>
112
+
113
+ /**
114
+ * Fetch events for many containers in a single call (N4 /unit accepts a
115
+ * comma-separated UNIT_NBR list). Optional: callers feature-detect and fall
116
+ * back to per-container `fetchEvents`. Each returned event carries its own
117
+ * `containerNumber` for grouping back onto the originating job.
118
+ */
119
+ fetchEventsBatch?(input: {
120
+ containerNumbers: string[]
121
+ config: ResolvedTerminalConfig
122
+ }): Promise<{ events: TerminalFetchedEvent[] }>
123
+
124
+ /**
125
+ * Resolve a single vessel visit by its visit reference. Optional: adapters
126
+ * that don't support a vessel endpoint omit it, and callers must feature-detect
127
+ * before invoking. Returns null when the endpoint is unconfigured or the visit
128
+ * is not found.
129
+ */
130
+ fetchVesselVisit?(input: {
131
+ visitRef: string
132
+ config: ResolvedTerminalConfig
133
+ }): Promise<NormalizedVesselVisit | null>
134
+
135
+ testConnection(config: ResolvedTerminalConfig): Promise<TerminalAdapterTestResult>
136
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * TOTP (RFC 6238) code generation for terminal adapters whose login expects an
3
+ * authenticator one-time code in place of a static password (e.g. GCT).
4
+ *
5
+ * This is a deliberate, self-contained COPY of the generation half of
6
+ * `packages/two-factor/src/modules/two_factor/lib/totp.ts` — kept in sync by
7
+ * hand. We copy rather than depend on `@freighttech/two-factor` so this backend
8
+ * tracking module stays dependency-light (only `fuse.js` + `undici`) and free of
9
+ * that package's Next/React/mikro-orm peer deps. The algorithm is pure Node
10
+ * `crypto`; only the consume-side pieces are copied (base32 decode + HOTP +
11
+ * `generateTotp`) — enrollment helpers (secret/QR generation, verification) live
12
+ * in the two-factor package and are not needed here.
13
+ *
14
+ * Parameters match a standard authenticator app: SHA1, 6 digits, 30s period.
15
+ */
16
+ import crypto from 'crypto'
17
+
18
+ // ── Base32 (RFC 4648, no padding) ──────────────────────────────────────────
19
+ const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
20
+
21
+ /** Decode a base32 authenticator secret into its raw key bytes. */
22
+ export function base32Decode(input: string): Buffer {
23
+ const clean = input.replace(/=+$/g, '').replace(/\s+/g, '').toUpperCase()
24
+ let bits = 0
25
+ let value = 0
26
+ const bytes: number[] = []
27
+ for (const ch of clean) {
28
+ const idx = BASE32_ALPHABET.indexOf(ch)
29
+ if (idx === -1) throw new Error('totp: invalid base32 character in secret')
30
+ value = (value << 5) | idx
31
+ bits += 5
32
+ if (bits >= 8) {
33
+ bytes.push((value >>> (bits - 8)) & 0xff)
34
+ bits -= 8
35
+ }
36
+ }
37
+ return Buffer.from(bytes)
38
+ }
39
+
40
+ // ── TOTP (RFC 6238) over HOTP (RFC 4226) ────────────────────────────────────
41
+ export const TOTP_PERIOD_SECONDS = 30
42
+ export const TOTP_DIGITS = 6
43
+
44
+ function hotp(secret: Buffer, counter: number): string {
45
+ const buf = Buffer.alloc(8)
46
+ // 64-bit big-endian counter (JS bitops are 32-bit, so split hi/lo).
47
+ buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0)
48
+ buf.writeUInt32BE(counter >>> 0, 4)
49
+ const hmac = crypto.createHmac('sha1', secret).update(buf).digest()
50
+ const offset = hmac[hmac.length - 1] & 0xf
51
+ const binary =
52
+ ((hmac[offset] & 0x7f) << 24) |
53
+ ((hmac[offset + 1] & 0xff) << 16) |
54
+ ((hmac[offset + 2] & 0xff) << 8) |
55
+ (hmac[offset + 3] & 0xff)
56
+ return (binary % 10 ** TOTP_DIGITS).toString().padStart(TOTP_DIGITS, '0')
57
+ }
58
+
59
+ /** Compute the current TOTP code for a base32 secret (default time: now). */
60
+ export function generateTotp(base32Secret: string, nowMs: number = Date.now()): string {
61
+ const counter = Math.floor(nowMs / 1000 / TOTP_PERIOD_SECONDS)
62
+ return hotp(base32Decode(base32Secret), counter)
63
+ }
@@ -0,0 +1,104 @@
1
+ import { Migration } from '@mikro-orm/migrations'
2
+
3
+ export class Migration20260609120000_terminal_tracking extends Migration {
4
+ override async up(): Promise<void> {
5
+ // ─── terminal_tracking_terminal_configs ──────────────────
6
+ this.addSql(`create table if not exists "terminal_tracking_terminal_configs" (
7
+ "id" uuid not null default gen_random_uuid(),
8
+ "organization_id" uuid not null,
9
+ "tenant_id" uuid not null,
10
+ "terminal_code" text not null,
11
+ "adapter_type" text not null,
12
+ "display_name" text not null,
13
+ "base_url" text not null,
14
+ "endpoints" jsonb not null,
15
+ "auth_type" text not null,
16
+ "token_url" text null,
17
+ "scope" text null,
18
+ "client_id" text null,
19
+ "auth_config" jsonb null,
20
+ "rate_limit_requests" integer not null default 200,
21
+ "rate_limit_window_seconds" integer not null default 60,
22
+ "unlocode" varchar(5) null,
23
+ "bic_codes" jsonb null,
24
+ "smdg_codes" jsonb null,
25
+ "name_aliases" jsonb null,
26
+ "is_active" boolean not null default true,
27
+ "created_at" timestamptz not null default now(),
28
+ "updated_at" timestamptz not null default now(),
29
+ "deleted_at" timestamptz null,
30
+ constraint "terminal_tracking_terminal_configs_pkey" primary key ("id")
31
+ );`)
32
+ this.addSql(`create index if not exists "tt_terminal_configs_org_tenant_idx" on "terminal_tracking_terminal_configs" ("organization_id", "tenant_id");`)
33
+ this.addSql(`create unique index if not exists "tt_terminal_configs_code_uniq" on "terminal_tracking_terminal_configs" ("organization_id", "tenant_id", "terminal_code");`)
34
+
35
+ // ─── terminal_tracking_jobs ──────────────────────────────
36
+ this.addSql(`create table if not exists "terminal_tracking_jobs" (
37
+ "id" uuid not null default gen_random_uuid(),
38
+ "organization_id" uuid not null,
39
+ "tenant_id" uuid not null,
40
+ "terminal_code" text not null,
41
+ "container_number" text not null,
42
+ "status" text not null default 'active',
43
+ "schedule" jsonb null,
44
+ "next_poll_at" timestamptz null,
45
+ "last_poll_at" timestamptz null,
46
+ "retry_count" integer not null default 0,
47
+ "error_history" jsonb null,
48
+ "created_at" timestamptz not null default now(),
49
+ "updated_at" timestamptz not null default now(),
50
+ "deleted_at" timestamptz null,
51
+ constraint "terminal_tracking_jobs_pkey" primary key ("id")
52
+ );`)
53
+ this.addSql(`create index if not exists "tt_jobs_org_tenant_idx" on "terminal_tracking_jobs" ("organization_id", "tenant_id");`)
54
+ this.addSql(`create index if not exists "tt_jobs_status_idx" on "terminal_tracking_jobs" ("status");`)
55
+ this.addSql(`create index if not exists "tt_jobs_next_poll_idx" on "terminal_tracking_jobs" ("next_poll_at");`)
56
+ this.addSql(`create index if not exists "tt_jobs_container_idx" on "terminal_tracking_jobs" ("container_number");`)
57
+
58
+ // ─── terminal_tracking_events ────────────────────────────
59
+ this.addSql(`create table if not exists "terminal_tracking_events" (
60
+ "id" uuid not null default gen_random_uuid(),
61
+ "organization_id" uuid not null,
62
+ "tenant_id" uuid not null,
63
+ "job_id" uuid not null,
64
+ "source" text not null default 'terminal',
65
+ "source_event_id" text not null,
66
+ "event_type" text not null,
67
+ "event_code" text not null,
68
+ "event_classifier_code" text null,
69
+ "event_date_time" timestamptz not null,
70
+ "container_number" text not null,
71
+ "ufv_gkey" text not null,
72
+ "transit_state" text null,
73
+ "visit_state" text null,
74
+ "facility_code" text null,
75
+ "facility_code_list_provider" text null,
76
+ "unlocode" varchar(5) null,
77
+ "visit_ref_in" text null,
78
+ "visit_ref_out" text null,
79
+ "vessel_name" text null,
80
+ "voyage_number" text null,
81
+ "mode_of_transport" text null,
82
+ "seals" jsonb null,
83
+ "vgm_weight_kg" double precision null,
84
+ "impediments" jsonb null,
85
+ "raw_data" jsonb null,
86
+ "created_at" timestamptz not null default now(),
87
+ constraint "terminal_tracking_events_pkey" primary key ("id")
88
+ );`)
89
+ this.addSql(`do $$ begin alter table "terminal_tracking_events"
90
+ add constraint "terminal_tracking_events_job_id_foreign"
91
+ foreign key ("job_id") references "terminal_tracking_jobs" ("id") on update cascade on delete cascade; exception when duplicate_object then null; when duplicate_table then null; end $$;`)
92
+ this.addSql(`create index if not exists "tt_events_org_tenant_idx" on "terminal_tracking_events" ("organization_id", "tenant_id");`)
93
+ this.addSql(`create index if not exists "tt_events_job_idx" on "terminal_tracking_events" ("job_id");`)
94
+ this.addSql(`create index if not exists "tt_events_container_idx" on "terminal_tracking_events" ("container_number");`)
95
+ this.addSql(`create index if not exists "tt_events_event_date_idx" on "terminal_tracking_events" ("event_date_time");`)
96
+ this.addSql(`create unique index if not exists "tt_events_source_event_uniq" on "terminal_tracking_events" ("job_id", "source", "source_event_id");`)
97
+ }
98
+
99
+ override async down(): Promise<void> {
100
+ this.addSql(`drop table if exists "terminal_tracking_events" cascade;`)
101
+ this.addSql(`drop table if exists "terminal_tracking_jobs" cascade;`)
102
+ this.addSql(`drop table if exists "terminal_tracking_terminal_configs" cascade;`)
103
+ }
104
+ }
@@ -0,0 +1,40 @@
1
+ import { Migration } from '@mikro-orm/migrations'
2
+
3
+ export class Migration20260610120000_terminal_vessel_visits extends Migration {
4
+ override async up(): Promise<void> {
5
+ // ─── terminal_tracking_vessel_visits ─────────────────────
6
+ this.addSql(`create table if not exists "terminal_tracking_vessel_visits" (
7
+ "id" uuid not null default gen_random_uuid(),
8
+ "organization_id" uuid not null,
9
+ "tenant_id" uuid not null,
10
+ "terminal_code" text not null,
11
+ "visit_ref" text not null,
12
+ "vessel_name" text null,
13
+ "ib_voyage" text null,
14
+ "ob_voyage" text null,
15
+ "line" text null,
16
+ "phase" text null,
17
+ "eta" timestamptz null,
18
+ "etd" timestamptz null,
19
+ "ata" timestamptz null,
20
+ "atd" timestamptz null,
21
+ "begin_receive" timestamptz null,
22
+ "dry_cutoff" timestamptz null,
23
+ "raw_data" jsonb null,
24
+ "created_at" timestamptz not null default now(),
25
+ "updated_at" timestamptz not null default now(),
26
+ constraint "terminal_tracking_vessel_visits_pkey" primary key ("id")
27
+ );`)
28
+ this.addSql(`create index if not exists "tt_vessel_visits_org_tenant_idx" on "terminal_tracking_vessel_visits" ("organization_id", "tenant_id");`)
29
+ this.addSql(`create index if not exists "tt_vessel_visits_visit_ref_idx" on "terminal_tracking_vessel_visits" ("visit_ref");`)
30
+ this.addSql(`create unique index if not exists "tt_vessel_visits_uniq" on "terminal_tracking_vessel_visits" ("organization_id", "tenant_id", "terminal_code", "visit_ref");`)
31
+
32
+ // ─── terminal_tracking_terminal_configs: vessel cache TTL ─
33
+ this.addSql(`alter table "terminal_tracking_terminal_configs" add column if not exists "vessel_cache_ttl_seconds" integer null;`)
34
+ }
35
+
36
+ override async down(): Promise<void> {
37
+ this.addSql(`alter table "terminal_tracking_terminal_configs" drop column if exists "vessel_cache_ttl_seconds";`)
38
+ this.addSql(`drop table if exists "terminal_tracking_vessel_visits" cascade;`)
39
+ }
40
+ }
@@ -0,0 +1,11 @@
1
+ import { Migration } from '@mikro-orm/migrations'
2
+
3
+ export class Migration20260611120000_terminal_config_proxy extends Migration {
4
+ override async up(): Promise<void> {
5
+ this.addSql(`alter table "terminal_tracking_terminal_configs" add column if not exists "proxy_url" text null;`)
6
+ }
7
+
8
+ override async down(): Promise<void> {
9
+ this.addSql(`alter table "terminal_tracking_terminal_configs" drop column if exists "proxy_url";`)
10
+ }
11
+ }
@@ -0,0 +1,13 @@
1
+ import { Migration } from '@mikro-orm/migrations'
2
+
3
+ export class Migration20260707120000_terminal_job_availability_markers extends Migration {
4
+ override async up(): Promise<void> {
5
+ this.addSql(`alter table "terminal_tracking_jobs" add column if not exists "empty_ready_at" timestamptz null;`)
6
+ this.addSql(`alter table "terminal_tracking_jobs" add column if not exists "holds_cleared_at" timestamptz null;`)
7
+ }
8
+
9
+ override async down(): Promise<void> {
10
+ this.addSql(`alter table "terminal_tracking_jobs" drop column if exists "empty_ready_at";`)
11
+ this.addSql(`alter table "terminal_tracking_jobs" drop column if exists "holds_cleared_at";`)
12
+ }
13
+ }
@@ -0,0 +1,16 @@
1
+ import { Migration } from '@mikro-orm/migrations'
2
+
3
+ /**
4
+ * Additive-only: a nullable `loaded_at` timestamp on terminal_tracking_events.
5
+ * The N4 `Loaded` column parsed into a typed datetime, captured on every event
6
+ * regardless of the container's current transit state.
7
+ */
8
+ export class Migration20260805120000_terminal_event_loaded_at extends Migration {
9
+ override async up(): Promise<void> {
10
+ this.addSql(`alter table "terminal_tracking_events" add column if not exists "loaded_at" timestamptz null;`)
11
+ }
12
+
13
+ override async down(): Promise<void> {
14
+ this.addSql(`alter table "terminal_tracking_events" drop column if exists "loaded_at";`)
15
+ }
16
+ }