@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,386 @@
1
+ import { Collection, OptionalProps } from '@mikro-orm/core'
2
+ import { Entity, PrimaryKey, Property, Index, Unique, ManyToOne, OneToMany } from '@mikro-orm/decorators/legacy'
3
+
4
+ // ─── Enums / shared types ────────────────────────────────────
5
+ // Re-declared locally on purpose: terminal_tracking must NOT depend on
6
+ // shipment_tracking internals. The cross-module contract is the event payload.
7
+
8
+ export type TerminalEventType = 'EQUIPMENT' | 'TRANSPORT'
9
+ export type TerminalEventClassifierCode = 'ACT' | 'PLN' | 'EST'
10
+ export type TerminalEventSource = 'terminal'
11
+ export type FacilityCodeProvider = 'SMDG' | 'BIC'
12
+ export type TerminalModeOfTransport = 'VESSEL' | 'RAIL' | 'TRUCK' | 'BARGE'
13
+
14
+ export type TerminalAuthType = 'oauth2_password' | 'oauth2_client_credentials' | 'gct_token' | 'basic'
15
+
16
+ export type TerminalTrackingJobStatus = 'active' | 'paused' | 'deactivated' | 'failed' | 'completed'
17
+
18
+ export type TerminalEndpoints = {
19
+ unit: string
20
+ vessel?: string
21
+ trainVisits?: string
22
+ }
23
+
24
+ export type TerminalSealInfo = {
25
+ number: string
26
+ type?: string | null
27
+ source?: string | null
28
+ }
29
+
30
+ // ─── TerminalConfig ──────────────────────────────────────────
31
+ // Per (org, tenant, terminalCode) terminal definition + tenant credentials.
32
+ // `auth_config` is encrypted at rest (see encryption.ts). `client_id`,
33
+ // `token_url`, `scope` are NOT secrets and stay as plain columns.
34
+
35
+ @Entity({ tableName: 'terminal_tracking_terminal_configs' })
36
+ @Index({ name: 'tt_terminal_configs_org_tenant_idx', properties: ['organizationId', 'tenantId'] })
37
+ @Unique({ name: 'tt_terminal_configs_code_uniq', properties: ['organizationId', 'tenantId', 'terminalCode'] })
38
+ export class TerminalConfig {
39
+ [OptionalProps]?:
40
+ | 'isActive'
41
+ | 'createdAt'
42
+ | 'updatedAt'
43
+ | 'deletedAt'
44
+ | 'rateLimitRequests'
45
+ | 'rateLimitWindowSeconds'
46
+ | 'vesselCacheTtlSeconds'
47
+ | 'proxyUrl'
48
+
49
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
50
+ id!: string
51
+
52
+ @Property({ name: 'organization_id', type: 'uuid' })
53
+ organizationId!: string
54
+
55
+ @Property({ name: 'tenant_id', type: 'uuid' })
56
+ tenantId!: string
57
+
58
+ // Lowercase terminal identifier, e.g. 'bct'
59
+ @Property({ name: 'terminal_code', type: 'text' })
60
+ terminalCode!: string
61
+
62
+ // Selects the adapter implementation from the registry, e.g. 'n4'
63
+ @Property({ name: 'adapter_type', type: 'text' })
64
+ adapterType!: string
65
+
66
+ @Property({ name: 'display_name', type: 'text' })
67
+ displayName!: string
68
+
69
+ @Property({ name: 'base_url', type: 'text' })
70
+ baseUrl!: string
71
+
72
+ // Optional HTTP(S) forward-proxy URL for this terminal's API calls (used to
73
+ // egress from a whitelisted IP). Null = direct. No embedded credentials —
74
+ // secure the proxy by IP allowlist.
75
+ @Property({ name: 'proxy_url', type: 'text', nullable: true })
76
+ proxyUrl?: string | null
77
+
78
+ @Property({ name: 'endpoints', type: 'jsonb' })
79
+ endpoints!: TerminalEndpoints
80
+
81
+ @Property({ name: 'auth_type', type: 'text' })
82
+ authType!: TerminalAuthType
83
+
84
+ // OAuth token endpoint (may include a policy query param, e.g. ?p=b2c_1_ropc_login)
85
+ @Property({ name: 'token_url', type: 'text', nullable: true })
86
+ tokenUrl?: string | null
87
+
88
+ @Property({ name: 'scope', type: 'text', nullable: true })
89
+ scope?: string | null
90
+
91
+ // Public OAuth client id (NOT a secret)
92
+ @Property({ name: 'client_id', type: 'text', nullable: true })
93
+ clientId?: string | null
94
+
95
+ // Encrypted at rest: { username, password } (ROPC) or { clientSecret }
96
+ @Property({ name: 'auth_config', type: 'jsonb', nullable: true })
97
+ authConfig?: Record<string, unknown> | null
98
+
99
+ @Property({ name: 'rate_limit_requests', type: 'integer', default: 200 })
100
+ rateLimitRequests: number = 200
101
+
102
+ @Property({ name: 'rate_limit_window_seconds', type: 'integer', default: 60 })
103
+ rateLimitWindowSeconds: number = 60
104
+
105
+ // TTL (seconds) for the cached /VESSEL visit lookup; null = module default.
106
+ @Property({ name: 'vessel_cache_ttl_seconds', type: 'integer', nullable: true })
107
+ vesselCacheTtlSeconds?: number | null
108
+
109
+ // ─── Matching fields (resolve a facility -> this terminal) ────
110
+ @Property({ name: 'unlocode', type: 'text', length: 5, nullable: true })
111
+ unlocode?: string | null
112
+
113
+ @Property({ name: 'bic_codes', type: 'jsonb', nullable: true })
114
+ bicCodes?: string[] | null
115
+
116
+ @Property({ name: 'smdg_codes', type: 'jsonb', nullable: true })
117
+ smdgCodes?: string[] | null
118
+
119
+ @Property({ name: 'name_aliases', type: 'jsonb', nullable: true })
120
+ nameAliases?: string[] | null
121
+
122
+ @Property({ name: 'is_active', type: 'boolean', default: true })
123
+ isActive: boolean = true
124
+
125
+ @Property({ name: 'created_at', type: Date, defaultRaw: 'now()' })
126
+ createdAt!: Date
127
+
128
+ @Property({ name: 'updated_at', type: Date, defaultRaw: 'now()', onUpdate: () => new Date() })
129
+ updatedAt!: Date
130
+
131
+ @Property({ name: 'deleted_at', type: Date, nullable: true })
132
+ deletedAt?: Date | null
133
+ }
134
+
135
+ // ─── TerminalTrackingJob ─────────────────────────────────────
136
+ // A module-owned watchlist unit: a container number tracked at one terminal.
137
+
138
+ @Entity({ tableName: 'terminal_tracking_jobs' })
139
+ @Index({ name: 'tt_jobs_org_tenant_idx', properties: ['organizationId', 'tenantId'] })
140
+ @Index({ name: 'tt_jobs_status_idx', properties: ['status'] })
141
+ @Index({ name: 'tt_jobs_next_poll_idx', properties: ['nextPollAt'] })
142
+ @Index({ name: 'tt_jobs_container_idx', properties: ['containerNumber'] })
143
+ export class TerminalTrackingJob {
144
+ [OptionalProps]?:
145
+ | 'createdAt'
146
+ | 'updatedAt'
147
+ | 'deletedAt'
148
+ | 'status'
149
+ | 'retryCount'
150
+ | 'emptyReadyAt'
151
+ | 'holdsClearedAt'
152
+
153
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
154
+ id!: string
155
+
156
+ @Property({ name: 'organization_id', type: 'uuid' })
157
+ organizationId!: string
158
+
159
+ @Property({ name: 'tenant_id', type: 'uuid' })
160
+ tenantId!: string
161
+
162
+ @Property({ name: 'terminal_code', type: 'text' })
163
+ terminalCode!: string
164
+
165
+ @Property({ name: 'container_number', type: 'text' })
166
+ containerNumber!: string
167
+
168
+ @Property({ type: 'text', default: 'active' })
169
+ status: TerminalTrackingJobStatus = 'active'
170
+
171
+ // JSON array of scheduled poll dates (ISO); null = follow the module default cadence
172
+ @Property({ type: 'jsonb', nullable: true })
173
+ schedule?: string[] | null
174
+
175
+ @Property({ name: 'next_poll_at', type: Date, nullable: true })
176
+ nextPollAt?: Date | null
177
+
178
+ @Property({ name: 'last_poll_at', type: Date, nullable: true })
179
+ lastPollAt?: Date | null
180
+
181
+ @Property({ name: 'retry_count', type: 'integer', default: 0 })
182
+ retryCount: number = 0
183
+
184
+ @Property({ name: 'error_history', type: 'jsonb', nullable: true })
185
+ errorHistory?: Array<{ date: string; message: string }> | null
186
+
187
+ // One-shot availability markers. Set the first time this container satisfies
188
+ // the availability rule at the terminal, so the corresponding notification
189
+ // fires exactly once. `empty_ready` = export empty collectable;
190
+ // `holds_cleared` = import container's blocking holds removed.
191
+ @Property({ name: 'empty_ready_at', type: Date, nullable: true })
192
+ emptyReadyAt?: Date | null
193
+
194
+ @Property({ name: 'holds_cleared_at', type: Date, nullable: true })
195
+ holdsClearedAt?: Date | null
196
+
197
+ @Property({ name: 'created_at', type: Date, defaultRaw: 'now()' })
198
+ createdAt!: Date
199
+
200
+ @Property({ name: 'updated_at', type: Date, defaultRaw: 'now()', onUpdate: () => new Date() })
201
+ updatedAt!: Date
202
+
203
+ @Property({ name: 'deleted_at', type: Date, nullable: true })
204
+ deletedAt?: Date | null
205
+
206
+ @OneToMany(() => TerminalEvent, (e) => e.job)
207
+ events = new Collection<TerminalEvent>(this)
208
+ }
209
+
210
+ // ─── TerminalEvent ───────────────────────────────────────────
211
+ // A normalized terminal event. `source_event_id` embeds the N4 Ufv_Gkey so
212
+ // that multiple Unit Facility Visits per container (import + export legs)
213
+ // remain distinct under the unique constraint.
214
+
215
+ @Entity({ tableName: 'terminal_tracking_events' })
216
+ @Index({ name: 'tt_events_org_tenant_idx', properties: ['organizationId', 'tenantId'] })
217
+ @Index({ name: 'tt_events_job_idx', properties: ['job'] })
218
+ @Index({ name: 'tt_events_container_idx', properties: ['containerNumber'] })
219
+ @Index({ name: 'tt_events_event_date_idx', properties: ['eventDateTime'] })
220
+ @Unique({ name: 'tt_events_source_event_uniq', properties: ['job', 'source', 'sourceEventId'] })
221
+ export class TerminalEvent {
222
+ [OptionalProps]?: 'createdAt' | 'source'
223
+
224
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
225
+ id!: string
226
+
227
+ @Property({ name: 'organization_id', type: 'uuid' })
228
+ organizationId!: string
229
+
230
+ @Property({ name: 'tenant_id', type: 'uuid' })
231
+ tenantId!: string
232
+
233
+ @ManyToOne(() => TerminalTrackingJob, { fieldName: 'job_id' })
234
+ job!: TerminalTrackingJob
235
+
236
+ @Property({ type: 'text', default: 'terminal' })
237
+ source: TerminalEventSource = 'terminal'
238
+
239
+ // '{terminalCode}:{ufvGkey}:{eventCode}'
240
+ @Property({ name: 'source_event_id', type: 'text' })
241
+ sourceEventId!: string
242
+
243
+ @Property({ name: 'event_type', type: 'text' })
244
+ eventType!: TerminalEventType
245
+
246
+ @Property({ name: 'event_code', type: 'text' })
247
+ eventCode!: string
248
+
249
+ @Property({ name: 'event_classifier_code', type: 'text', nullable: true })
250
+ eventClassifierCode?: TerminalEventClassifierCode | null
251
+
252
+ @Property({ name: 'event_date_time', type: Date })
253
+ eventDateTime!: Date
254
+
255
+ @Property({ name: 'container_number', type: 'text' })
256
+ containerNumber!: string
257
+
258
+ @Property({ name: 'ufv_gkey', type: 'text' })
259
+ ufvGkey!: string
260
+
261
+ @Property({ name: 'transit_state', type: 'text', nullable: true })
262
+ transitState?: string | null
263
+
264
+ @Property({ name: 'visit_state', type: 'text', nullable: true })
265
+ visitState?: string | null
266
+
267
+ // ─── Location ─────────────────────────────────────────────
268
+ @Property({ name: 'facility_code', type: 'text', nullable: true })
269
+ facilityCode?: string | null
270
+
271
+ @Property({ name: 'facility_code_list_provider', type: 'text', nullable: true })
272
+ facilityCodeListProvider?: FacilityCodeProvider | null
273
+
274
+ @Property({ name: 'unlocode', type: 'text', length: 5, nullable: true })
275
+ unlocode?: string | null
276
+
277
+ // ─── Visit references (for vessel/train enrichment) ───────
278
+ @Property({ name: 'visit_ref_in', type: 'text', nullable: true })
279
+ visitRefIn?: string | null
280
+
281
+ @Property({ name: 'visit_ref_out', type: 'text', nullable: true })
282
+ visitRefOut?: string | null
283
+
284
+ // ─── Transport ────────────────────────────────────────────
285
+ @Property({ name: 'vessel_name', type: 'text', nullable: true })
286
+ vesselName?: string | null
287
+
288
+ @Property({ name: 'voyage_number', type: 'text', nullable: true })
289
+ voyageNumber?: string | null
290
+
291
+ @Property({ name: 'mode_of_transport', type: 'text', nullable: true })
292
+ modeOfTransport?: TerminalModeOfTransport | null
293
+
294
+ // ─── Cargo extras ─────────────────────────────────────────
295
+ @Property({ name: 'seals', type: 'jsonb', nullable: true })
296
+ seals?: TerminalSealInfo[] | null
297
+
298
+ @Property({ name: 'vgm_weight_kg', type: 'float', nullable: true })
299
+ vgmWeightKg?: number | null
300
+
301
+ @Property({ name: 'impediments', type: 'jsonb', nullable: true })
302
+ impediments?: string[] | null
303
+
304
+ // When the container was loaded onto its transport (N4 `Loaded` column),
305
+ // parsed regardless of current transit state. Export = onto vessel; import =
306
+ // onto road/rail.
307
+ @Property({ name: 'loaded_at', type: Date, nullable: true })
308
+ loadedAt?: Date | null
309
+
310
+ @Property({ name: 'raw_data', type: 'jsonb', nullable: true })
311
+ rawData?: Record<string, unknown> | null
312
+
313
+ @Property({ name: 'created_at', type: Date, defaultRaw: 'now()' })
314
+ createdAt!: Date
315
+ }
316
+
317
+ // ─── TerminalVesselVisit ─────────────────────────────────────
318
+ // A resolved N4 vessel visit (from /VESSEL), keyed by visit ref and shared by
319
+ // every container on that visit. Doubles as a TTL cache: `updatedAt` is the
320
+ // freshness signal. Upsert-only (no soft delete); no FK to job.
321
+
322
+ @Entity({ tableName: 'terminal_tracking_vessel_visits' })
323
+ @Index({ name: 'tt_vessel_visits_org_tenant_idx', properties: ['organizationId', 'tenantId'] })
324
+ @Index({ name: 'tt_vessel_visits_visit_ref_idx', properties: ['visitRef'] })
325
+ @Unique({ name: 'tt_vessel_visits_uniq', properties: ['organizationId', 'tenantId', 'terminalCode', 'visitRef'] })
326
+ export class TerminalVesselVisit {
327
+ [OptionalProps]?: 'createdAt' | 'updatedAt'
328
+
329
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
330
+ id!: string
331
+
332
+ @Property({ name: 'organization_id', type: 'uuid' })
333
+ organizationId!: string
334
+
335
+ @Property({ name: 'tenant_id', type: 'uuid' })
336
+ tenantId!: string
337
+
338
+ @Property({ name: 'terminal_code', type: 'text' })
339
+ terminalCode!: string
340
+
341
+ // The N4 visit ref (the `Visit` column); equals an event's visitRefIn/Out.
342
+ @Property({ name: 'visit_ref', type: 'text' })
343
+ visitRef!: string
344
+
345
+ @Property({ name: 'vessel_name', type: 'text', nullable: true })
346
+ vesselName?: string | null
347
+
348
+ @Property({ name: 'ib_voyage', type: 'text', nullable: true })
349
+ ibVoyage?: string | null
350
+
351
+ @Property({ name: 'ob_voyage', type: 'text', nullable: true })
352
+ obVoyage?: string | null
353
+
354
+ @Property({ name: 'line', type: 'text', nullable: true })
355
+ line?: string | null
356
+
357
+ @Property({ name: 'phase', type: 'text', nullable: true })
358
+ phase?: string | null
359
+
360
+ @Property({ name: 'eta', type: Date, nullable: true })
361
+ eta?: Date | null
362
+
363
+ @Property({ name: 'etd', type: Date, nullable: true })
364
+ etd?: Date | null
365
+
366
+ @Property({ name: 'ata', type: Date, nullable: true })
367
+ ata?: Date | null
368
+
369
+ @Property({ name: 'atd', type: Date, nullable: true })
370
+ atd?: Date | null
371
+
372
+ @Property({ name: 'begin_receive', type: Date, nullable: true })
373
+ beginReceive?: Date | null
374
+
375
+ @Property({ name: 'dry_cutoff', type: Date, nullable: true })
376
+ dryCutoff?: Date | null
377
+
378
+ @Property({ name: 'raw_data', type: 'jsonb', nullable: true })
379
+ rawData?: Record<string, unknown> | null
380
+
381
+ @Property({ name: 'created_at', type: Date, defaultRaw: 'now()' })
382
+ createdAt!: Date
383
+
384
+ @Property({ name: 'updated_at', type: Date, defaultRaw: 'now()', onUpdate: () => new Date() })
385
+ updatedAt!: Date
386
+ }
@@ -0,0 +1,113 @@
1
+ import { z } from 'zod'
2
+
3
+ const endpointsSchema = z.object({
4
+ unit: z.string().min(1),
5
+ vessel: z.string().optional(),
6
+ trainVisits: z.string().optional(),
7
+ })
8
+
9
+ const authTypeSchema = z.enum(['oauth2_password', 'oauth2_client_credentials', 'gct_token', 'basic'])
10
+
11
+ // ─── TerminalConfig ──────────────────────────────────────────
12
+
13
+ export const terminalConfigCreateSchema = z.object({
14
+ organizationId: z.string().uuid(),
15
+ tenantId: z.string().uuid(),
16
+ terminalCode: z.string().min(1).transform((s) => s.toLowerCase()),
17
+ adapterType: z.string().min(1),
18
+ displayName: z.string().min(1),
19
+ baseUrl: z.string().url(),
20
+ proxyUrl: z.string().url().nullish(),
21
+ endpoints: endpointsSchema.optional(),
22
+ authType: authTypeSchema,
23
+ tokenUrl: z.string().url().nullish(),
24
+ scope: z.string().nullish(),
25
+ clientId: z.string().nullish(),
26
+ authConfig: z.record(z.string(), z.unknown()).nullish(),
27
+ rateLimitRequests: z.number().int().positive().optional(),
28
+ rateLimitWindowSeconds: z.number().int().positive().optional(),
29
+ unlocode: z.string().length(5).nullish(),
30
+ bicCodes: z.array(z.string()).nullish(),
31
+ smdgCodes: z.array(z.string()).nullish(),
32
+ nameAliases: z.array(z.string()).nullish(),
33
+ isActive: z.boolean().optional(),
34
+ })
35
+ export type TerminalConfigCreateInput = z.infer<typeof terminalConfigCreateSchema>
36
+
37
+ export const terminalConfigUpdateSchema = terminalConfigCreateSchema
38
+ .partial()
39
+ .extend({ id: z.string().uuid() })
40
+ export type TerminalConfigUpdateInput = z.infer<typeof terminalConfigUpdateSchema>
41
+
42
+ export const terminalConfigDeleteSchema = z.object({
43
+ id: z.string().uuid(),
44
+ organizationId: z.string().uuid(),
45
+ tenantId: z.string().uuid(),
46
+ })
47
+ export type TerminalConfigDeleteInput = z.infer<typeof terminalConfigDeleteSchema>
48
+
49
+ export const terminalConfigTestSchema = z.object({
50
+ id: z.string().uuid(),
51
+ organizationId: z.string().uuid(),
52
+ tenantId: z.string().uuid(),
53
+ })
54
+ export type TerminalConfigTestInput = z.infer<typeof terminalConfigTestSchema>
55
+
56
+ export const terminalConfigListSchema = z.object({
57
+ page: z.coerce.number().int().positive().optional(),
58
+ pageSize: z.coerce.number().int().positive().max(100).optional(),
59
+ sortField: z.string().optional(),
60
+ sortDir: z.enum(['asc', 'desc']).optional(),
61
+ terminalCode: z.string().optional(),
62
+ isActive: z.string().optional(),
63
+ })
64
+
65
+ // ─── TerminalTrackingJob ─────────────────────────────────────
66
+
67
+ export const trackingJobCreateSchema = z.object({
68
+ organizationId: z.string().uuid(),
69
+ tenantId: z.string().uuid(),
70
+ terminalCode: z.string().min(1).transform((s) => s.toLowerCase()),
71
+ containerNumber: z.string().min(4).transform((s) => s.toUpperCase()),
72
+ schedule: z.array(z.string()).nullish(),
73
+ })
74
+ export type TrackingJobCreateInput = z.infer<typeof trackingJobCreateSchema>
75
+
76
+ export const trackingJobDeleteSchema = z.object({
77
+ id: z.string().uuid(),
78
+ organizationId: z.string().uuid(),
79
+ tenantId: z.string().uuid(),
80
+ })
81
+ export type TrackingJobDeleteInput = z.infer<typeof trackingJobDeleteSchema>
82
+
83
+ export const trackingJobPollSchema = z.object({
84
+ jobId: z.string().uuid(),
85
+ organizationId: z.string().uuid(),
86
+ tenantId: z.string().uuid(),
87
+ })
88
+ export type TrackingJobPollInput = z.infer<typeof trackingJobPollSchema>
89
+
90
+ export const trackingJobListSchema = z.object({
91
+ page: z.coerce.number().int().positive().optional(),
92
+ // DynamicTable sends both `limit` and `pageSize` (same value); accept either.
93
+ pageSize: z.coerce.number().int().positive().max(100).optional(),
94
+ limit: z.coerce.number().int().positive().max(100).optional(),
95
+ sortField: z.string().optional(),
96
+ sortDir: z.enum(['asc', 'desc']).optional(),
97
+ terminalCode: z.string().optional(),
98
+ containerNumber: z.string().optional(),
99
+ status: z.string().optional(),
100
+ // DynamicTable filter popover — JSON-stringified FilterRow[] sent as `?filters=`
101
+ filters: z.string().optional(),
102
+ })
103
+
104
+ // ─── TerminalVesselVisit ─────────────────────────────────────
105
+
106
+ export const vesselVisitListSchema = z.object({
107
+ page: z.coerce.number().int().positive().optional(),
108
+ pageSize: z.coerce.number().int().positive().max(100).optional(),
109
+ visitRef: z.string().optional(),
110
+ containerNumber: z.string().optional().transform((s) => (s ? s.toUpperCase() : s)),
111
+ terminalCode: z.string().optional(),
112
+ })
113
+ export type VesselVisitListInput = z.infer<typeof vesselVisitListSchema>
@@ -0,0 +1,46 @@
1
+ import { asClass } from 'awilix'
2
+ import type { AppContainer } from '@open-mercato/shared/lib/di/container'
3
+ import { TerminalRegistry } from './services/terminalRegistry'
4
+ import { TerminalTrackingService } from './services/terminalTrackingService'
5
+ import { TerminalMatcherService } from './services/terminalMatcherService'
6
+ import { registerAllAdapters } from './lib/adapters'
7
+
8
+ export function register(container: AppContainer) {
9
+ // Adapter registry (singleton - adapters registered once)
10
+ container.register({
11
+ terminalTrackingRegistry: asClass(TerminalRegistry).singleton(),
12
+ })
13
+
14
+ // Tracking service (poll loop, dedup, emit)
15
+ container.register({
16
+ terminalTrackingService: {
17
+ resolve: () => {
18
+ const cache = container.resolve<any>('cache')
19
+ return new TerminalTrackingService({
20
+ em: () => container.resolve('em'),
21
+ eventBus: container.resolve('eventBus'),
22
+ terminalRegistry: container.resolve('terminalTrackingRegistry'),
23
+ cacheService: {
24
+ get: (key: string) => cache.get(key),
25
+ set: (key: string, value: string, ttlSeconds?: number) =>
26
+ cache.set(key, value, ttlSeconds ? { ttl: ttlSeconds * 1000 } : undefined),
27
+ },
28
+ })
29
+ },
30
+ },
31
+ })
32
+
33
+ // Terminal matcher (fuzzy name + exact BIC/SMDG/UNLOCODE)
34
+ container.register({
35
+ terminalMatcherService: {
36
+ resolve: () =>
37
+ new TerminalMatcherService({
38
+ em: () => container.resolve('em'),
39
+ }),
40
+ },
41
+ })
42
+
43
+ // Register built-in adapters
44
+ const registry = container.resolve<TerminalRegistry>('terminalTrackingRegistry')
45
+ registerAllAdapters(registry)
46
+ }
@@ -0,0 +1,12 @@
1
+ import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
2
+
3
+ export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
4
+ {
5
+ entityId: 'terminal_tracking:terminal_config',
6
+ fields: [
7
+ { field: 'auth_config' },
8
+ ],
9
+ },
10
+ ]
11
+
12
+ export default defaultEncryptionMaps
@@ -0,0 +1,48 @@
1
+ import { createModuleEvents } from '@open-mercato/shared/modules/events'
2
+
3
+ const events = [
4
+ // ─── Tracking job lifecycle (internal) ─────────────────────────
5
+ { id: 'terminal_tracking.tracking_job.created', label: 'Terminal Tracking Job Created', entity: 'tracking_job', category: 'crud', excludeFromTriggers: true },
6
+ { id: 'terminal_tracking.tracking_job.updated', label: 'Terminal Tracking Job Updated', entity: 'tracking_job', category: 'crud', excludeFromTriggers: true },
7
+ { id: 'terminal_tracking.tracking_job.failed', label: 'Terminal Tracking Job Failed', entity: 'tracking_job', category: 'lifecycle', excludeFromTriggers: true },
8
+ { id: 'terminal_tracking.tracking_job.poll_failed', label: 'Terminal Tracking Job Poll Failed', entity: 'tracking_job', category: 'lifecycle', excludeFromTriggers: true },
9
+
10
+ // ─── Raw terminal event (the bridge trigger) ───────────────────
11
+ { id: 'terminal_tracking.terminal_event.created', label: 'Terminal Event Created', entity: 'terminal_event', category: 'crud' },
12
+ { id: 'terminal_tracking.terminal_event.updated', label: 'Terminal Event Updated', entity: 'terminal_event', category: 'crud' },
13
+
14
+ // ─── Semantic milestones (per container) ───────────────────────
15
+ { id: 'terminal_tracking.equipment.gate_in', label: 'Container Gate In (Terminal)', entity: 'terminal_event', category: 'lifecycle' },
16
+ { id: 'terminal_tracking.equipment.discharged', label: 'Container Discharged (Terminal)', entity: 'terminal_event', category: 'lifecycle' },
17
+ { id: 'terminal_tracking.equipment.loaded', label: 'Container Loaded (Terminal)', entity: 'terminal_event', category: 'lifecycle' },
18
+ { id: 'terminal_tracking.transport.departed', label: 'Container Departed (Terminal)', entity: 'terminal_event', category: 'lifecycle' },
19
+
20
+ // ─── Availability milestones (fire once per container) ─────────
21
+ // Export empty released for collection: empty container in the yard with no
22
+ // movement-blocking holds.
23
+ { id: 'terminal_tracking.equipment.empty_ready', label: 'Empty Container Ready For Pickup (Terminal)', entity: 'terminal_event', category: 'lifecycle' },
24
+ // Import container's blocking holds removed: it became collectable (transition
25
+ // from blocked → unblocked while in the yard).
26
+ { id: 'terminal_tracking.equipment.holds_cleared', label: 'Import Holds Cleared (Terminal)', entity: 'terminal_event', category: 'lifecycle' },
27
+ // The raw impediment (hold) set changed between polls. Unlike the two
28
+ // one-shot availability events above, this fires every poll the set changes
29
+ // (including first appearance with holds) so downstream consumers can mirror
30
+ // the current hold list. Payload carries `impediments: string[]`.
31
+ { id: 'terminal_tracking.equipment.holds_updated', label: 'Container Holds Updated (Terminal)', entity: 'terminal_event', category: 'lifecycle' },
32
+ // The per-mode load/pickup STOP flags (Stop-Vsl / Stop-Road / Stop-Rail)
33
+ // changed between polls. Like `holds_updated`, fires every poll the set
34
+ // changes (incl. first appearance) so downstream consumers can mirror the
35
+ // current stops. Payload carries `stops: { vsl, road, rail: boolean | null }`.
36
+ { id: 'terminal_tracking.equipment.stops_updated', label: 'Container Load Stops Updated (Terminal)', entity: 'terminal_event', category: 'lifecycle' },
37
+ ] as const
38
+
39
+ export const eventsConfig = createModuleEvents({
40
+ moduleId: 'terminal_tracking',
41
+ events,
42
+ })
43
+
44
+ export const emitTerminalTrackingEvent = eventsConfig.emit
45
+
46
+ export type TerminalTrackingEventId = typeof events[number]['id']
47
+
48
+ export default eventsConfig