@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,68 @@
1
+ import { expect, test } from '@playwright/test'
2
+ import {
3
+ login,
4
+ createBctTerminalConfig,
5
+ testTerminalConfig,
6
+ deleteTerminalConfig,
7
+ type AuthSession,
8
+ } from './helpers'
9
+
10
+ /**
11
+ * TC-TRACK-317 — BCT (Bałtycki Terminal Kontenerowy, Gdynia) / INCOS adapter
12
+ * wiring, exercised through the real host app.
13
+ *
14
+ * These do NOT hit the live INCOS API — test-connection fails deterministically
15
+ * (connection-refused loopback host). What they prove is that the Phase-1
16
+ * additions are wired end-to-end through the app:
17
+ * - the `basic` auth type + `bct` adapter pass create validation (validator +
18
+ * entity changes),
19
+ * - the `bct` adapter is registered and resolved by adapterType (registry) —
20
+ * the failure is a real adapter result, never "no adapter for type",
21
+ * - test-connection returns a STRUCTURED { success:false } payload, not a 500.
22
+ *
23
+ * Note: the exact failure message depends on the environment. Where tenant-data
24
+ * decryption of `auth_config` is active, the loopback host yields a transport
25
+ * error; where it is not (and creds resolve empty), the adapter's Basic-auth
26
+ * guard fires first. Both are structured failures — the assertions below hold
27
+ * either way and deliberately do not pin the first case's message text.
28
+ */
29
+ test.describe('TC-TRACK-317: BCT terminal-config test connection', () => {
30
+ let session: AuthSession
31
+ const createdIds: string[] = []
32
+
33
+ test.beforeAll(async ({ request }) => {
34
+ session = await login(request)
35
+ })
36
+
37
+ test.afterEach(async ({ request }) => {
38
+ while (createdIds.length) {
39
+ await deleteTerminalConfig(request, session, createdIds.pop() as string)
40
+ }
41
+ })
42
+
43
+ test('accepts a basic/bct config and reports a structured connection failure', async ({ request }) => {
44
+ const { id } = await createBctTerminalConfig(request, session)
45
+ createdIds.push(id)
46
+ expect(id).toBeTruthy()
47
+
48
+ const { status, body } = await testTerminalConfig(request, session, id)
49
+ // A structured 200 response — the adapter resolved and failed gracefully.
50
+ expect(status).toBe(200)
51
+ expect(body.success).toBe(false)
52
+ expect(typeof body.message).toBe('string')
53
+ expect(body.message).toBeTruthy()
54
+ // Registration proof: it is not the generic "no adapter for type" error.
55
+ expect(body.message?.toLowerCase()).not.toContain('no adapter')
56
+ })
57
+
58
+ test('surfaces missing BCT credentials as a clear failure message', async ({ request }) => {
59
+ const { id } = await createBctTerminalConfig(request, session, { authConfig: {} })
60
+ createdIds.push(id)
61
+
62
+ const { status, body } = await testTerminalConfig(request, session, id)
63
+ expect(status).toBe(200)
64
+ expect(body.success).toBe(false)
65
+ // The adapter's Basic-auth guard names the missing fields.
66
+ expect(body.message).toContain('username/password')
67
+ })
68
+ })
@@ -0,0 +1,148 @@
1
+ import type { APIRequestContext } from '@playwright/test'
2
+
3
+ export const BASE_URL = process.env.BASE_URL?.trim() || 'http://localhost:3000'
4
+
5
+ export interface AuthSession {
6
+ token: string
7
+ orgId: string
8
+ tenantId: string
9
+ userId?: string
10
+ }
11
+
12
+ function decodeJwt(token: string): Record<string, unknown> {
13
+ const payload = token.split('.')[1]
14
+ return JSON.parse(Buffer.from(payload, 'base64url').toString())
15
+ }
16
+
17
+ /**
18
+ * Log in via the host auth endpoint and return the JWT + scope claims.
19
+ * Credentials default to the local seed admin but are overridable via
20
+ * TEST_LOGIN_EMAIL / TEST_LOGIN_PASSWORD for other environments.
21
+ */
22
+ export async function login(
23
+ request: APIRequestContext,
24
+ email = process.env.TEST_LOGIN_EMAIL?.trim() || 'admin@acme.com',
25
+ password = process.env.TEST_LOGIN_PASSWORD?.trim() || 'secret',
26
+ ): Promise<AuthSession> {
27
+ const form = new URLSearchParams()
28
+ form.set('email', email)
29
+ form.set('password', password)
30
+ const response = await request.post(`${BASE_URL}/api/auth/login`, {
31
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
32
+ data: form.toString(),
33
+ })
34
+ if (!response.ok()) {
35
+ throw new Error(`Login failed: ${response.status()} ${await response.text()}`)
36
+ }
37
+ const body = await response.json()
38
+ if (!body?.token) throw new Error('Login response missing token')
39
+ const jwt = decodeJwt(body.token)
40
+ return {
41
+ token: body.token,
42
+ orgId: String(jwt.orgId ?? jwt.organizationId ?? ''),
43
+ tenantId: String(jwt.tenantId ?? ''),
44
+ userId: jwt.sub ? String(jwt.sub) : undefined,
45
+ }
46
+ }
47
+
48
+ export function authHeaders(token: string): Record<string, string> {
49
+ return { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
50
+ }
51
+
52
+ const CONFIG_API = `${BASE_URL}/api/terminal_tracking/terminal-configs`
53
+
54
+ /** A unique terminal code so parallel/repeat runs never collide on the unique index. */
55
+ export function uniqueTerminalCode(prefix = 'gcttest'): string {
56
+ const stamp = `${Date.now().toString().slice(-6)}${Math.floor(Math.random() * 1_000).toString().padStart(3, '0')}`
57
+ return `${prefix}-${stamp}`.toLowerCase()
58
+ }
59
+
60
+ /**
61
+ * Build a GCT terminal-config create payload. `baseUrl` defaults to a
62
+ * connection-refused loopback so `testConnection` fails fast and deterministically
63
+ * without touching a real terminal. `organizationId`/`tenantId` are injected
64
+ * server-side from the auth scope, so they are intentionally omitted here.
65
+ */
66
+ export function buildGctConfigPayload(overrides: Record<string, unknown> = {}): Record<string, unknown> {
67
+ return {
68
+ terminalCode: uniqueTerminalCode(),
69
+ adapterType: 'gct',
70
+ displayName: 'GCT Test',
71
+ baseUrl: 'http://127.0.0.1:1',
72
+ authType: 'gct_token',
73
+ endpoints: { unit: '/gctapi/GetContainerDetails' },
74
+ authConfig: { companyCode: 'ACME', loginName: 'user', loginPassword: 'secret' },
75
+ unlocode: 'PLGDY',
76
+ ...overrides,
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Build a BCT/INCOS terminal-config create payload. Like the GCT builder,
82
+ * `baseUrl` defaults to a connection-refused loopback so `testConnection` fails
83
+ * fast without touching the real INCOS host. Auth is HTTP Basic
84
+ * (`{ username, password }`). Daily-cap rate limit mirrors production
85
+ * (10000 requests / 86400s).
86
+ */
87
+ export function buildBctConfigPayload(overrides: Record<string, unknown> = {}): Record<string, unknown> {
88
+ return {
89
+ terminalCode: uniqueTerminalCode('bcttest'),
90
+ adapterType: 'bct',
91
+ displayName: 'BCT Test',
92
+ baseUrl: 'http://127.0.0.1:1',
93
+ authType: 'basic',
94
+ endpoints: { unit: '/rest-container/container', vessel: '/rest-vesselvisit/vesselvisit' },
95
+ authConfig: { username: 'user', password: 'secret' },
96
+ rateLimitRequests: 10000,
97
+ rateLimitWindowSeconds: 86400,
98
+ unlocode: 'PLBCT',
99
+ ...overrides,
100
+ }
101
+ }
102
+
103
+ export async function createTerminalConfig(
104
+ request: APIRequestContext,
105
+ session: AuthSession,
106
+ overrides: Record<string, unknown> = {},
107
+ build: (o: Record<string, unknown>) => Record<string, unknown> = buildGctConfigPayload,
108
+ ): Promise<{ id: string; payload: Record<string, unknown> }> {
109
+ const payload = build(overrides)
110
+ const res = await request.post(CONFIG_API, { headers: authHeaders(session.token), data: payload })
111
+ if (!res.ok()) throw new Error(`Create config failed: ${res.status()} ${await res.text()}`)
112
+ const body = (await res.json()) as { id: string }
113
+ return { id: body.id, payload }
114
+ }
115
+
116
+ /** Create a BCT/INCOS terminal config (Basic auth), reusing the shared POST. */
117
+ export async function createBctTerminalConfig(
118
+ request: APIRequestContext,
119
+ session: AuthSession,
120
+ overrides: Record<string, unknown> = {},
121
+ ): Promise<{ id: string; payload: Record<string, unknown> }> {
122
+ return createTerminalConfig(request, session, overrides, buildBctConfigPayload)
123
+ }
124
+
125
+ export async function testTerminalConfig(
126
+ request: APIRequestContext,
127
+ session: AuthSession,
128
+ id: string,
129
+ ): Promise<{ status: number; body: { success?: boolean; message?: string; latencyMs?: number } }> {
130
+ const res = await request.post(`${CONFIG_API}/test`, {
131
+ headers: authHeaders(session.token),
132
+ data: { id },
133
+ })
134
+ return { status: res.status(), body: res.ok() ? await res.json() : { message: await res.text() } }
135
+ }
136
+
137
+ /** Best-effort soft-delete used in teardown; never throws. */
138
+ export async function deleteTerminalConfig(
139
+ request: APIRequestContext,
140
+ session: AuthSession,
141
+ id: string,
142
+ ): Promise<void> {
143
+ try {
144
+ await request.delete(CONFIG_API, { headers: authHeaders(session.token), data: { id } })
145
+ } catch {
146
+ // teardown best-effort
147
+ }
148
+ }
@@ -0,0 +1,4 @@
1
+ export const integrationMeta = {
2
+ description: 'Terminal-tracking integration coverage',
3
+ dependsOnModules: ['terminal_tracking'],
4
+ }
@@ -0,0 +1,10 @@
1
+ export const features = [
2
+ { id: 'terminal_tracking.terminal_configs.view', title: 'View Terminal Configs', module: 'terminal_tracking' },
3
+ { id: 'terminal_tracking.terminal_configs.manage', title: 'Manage Terminal Configs', module: 'terminal_tracking' },
4
+ { id: 'terminal_tracking.tracking_jobs.view', title: 'View Terminal Tracking Jobs', module: 'terminal_tracking' },
5
+ { id: 'terminal_tracking.tracking_jobs.manage', title: 'Manage Terminal Tracking Jobs', module: 'terminal_tracking' },
6
+ { id: 'terminal_tracking.events.view', title: 'View Terminal Events', module: 'terminal_tracking' },
7
+ { id: 'terminal_tracking.settings.manage', title: 'Manage Terminal Tracking Settings', module: 'terminal_tracking' },
8
+ ]
9
+
10
+ export default features
@@ -0,0 +1,28 @@
1
+ import { type ZodTypeAny } from 'zod'
2
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
3
+ import {
4
+ createCrudOpenApiFactory,
5
+ createPagedListResponseSchema as createSharedPagedListResponseSchema,
6
+ defaultCreateResponseSchema as sharedDefaultCreateResponseSchema,
7
+ defaultOkResponseSchema as sharedDefaultOkResponseSchema,
8
+ type CrudOpenApiOptions,
9
+ } from '@open-mercato/shared/lib/openapi/crud'
10
+
11
+ export const defaultCreateResponseSchema = sharedDefaultCreateResponseSchema
12
+ export const defaultOkResponseSchema = sharedDefaultOkResponseSchema
13
+
14
+ export function createPagedListResponseSchema(itemSchema: ZodTypeAny) {
15
+ return createSharedPagedListResponseSchema(itemSchema, { paginationMetaOptional: true })
16
+ }
17
+
18
+ const buildTerminalTrackingCrudOpenApi = createCrudOpenApiFactory({
19
+ defaultTag: 'Terminal Tracking',
20
+ defaultCreateResponseSchema,
21
+ defaultOkResponseSchema,
22
+ makeListDescription: ({ pluralLower }) =>
23
+ `Returns a paginated collection of ${pluralLower} scoped to the authenticated organization.`,
24
+ })
25
+
26
+ export function createTerminalTrackingCrudOpenApi(options: CrudOpenApiOptions): OpenApiRouteDoc {
27
+ return buildTerminalTrackingCrudOpenApi(options)
28
+ }
@@ -0,0 +1,140 @@
1
+ import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
2
+ import { z } from 'zod'
3
+ import { TerminalConfig } from '../../data/entities'
4
+ import {
5
+ terminalConfigListSchema,
6
+ terminalConfigCreateSchema,
7
+ terminalConfigUpdateSchema,
8
+ } from '../../data/validators'
9
+ import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
10
+ import {
11
+ createTerminalTrackingCrudOpenApi,
12
+ createPagedListResponseSchema,
13
+ defaultOkResponseSchema,
14
+ } from '../openapi'
15
+ import { withScopedPayload } from '../utils'
16
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
17
+
18
+ const rawBodySchema = z.object({}).passthrough()
19
+
20
+ type ListQuery = z.infer<typeof terminalConfigListSchema>
21
+
22
+ const routeMetadata = {
23
+ GET: { requireAuth: true, requireFeatures: ['terminal_tracking.terminal_configs.view'] },
24
+ POST: { requireAuth: true, requireFeatures: ['terminal_tracking.terminal_configs.manage'] },
25
+ PUT: { requireAuth: true, requireFeatures: ['terminal_tracking.terminal_configs.manage'] },
26
+ DELETE: { requireAuth: true, requireFeatures: ['terminal_tracking.terminal_configs.manage'] },
27
+ }
28
+
29
+ const listFields = [
30
+ 'id',
31
+ 'terminalCode',
32
+ 'adapterType',
33
+ 'displayName',
34
+ 'baseUrl',
35
+ 'authType',
36
+ 'unlocode',
37
+ 'isActive',
38
+ 'createdAt',
39
+ 'updatedAt',
40
+ ]
41
+
42
+ const buildFilters = (query: ListQuery): Record<string, unknown> => {
43
+ const filters: Record<string, unknown> = { deletedAt: null }
44
+ if (query.terminalCode) filters.terminalCode = query.terminalCode
45
+ if (query.isActive !== undefined) {
46
+ const parsed = parseBooleanToken(query.isActive)
47
+ if (parsed !== null) filters.isActive = parsed
48
+ }
49
+ return filters
50
+ }
51
+
52
+ const crud = makeCrudRoute({
53
+ metadata: routeMetadata,
54
+ orm: {
55
+ entity: TerminalConfig,
56
+ idField: 'id',
57
+ orgField: 'organizationId',
58
+ tenantField: 'tenantId',
59
+ softDeleteField: 'deletedAt',
60
+ },
61
+ list: {
62
+ schema: terminalConfigListSchema,
63
+ fields: listFields,
64
+ sortFieldMap: {
65
+ id: 'id',
66
+ terminalCode: 'terminal_code',
67
+ isActive: 'is_active',
68
+ createdAt: 'created_at',
69
+ },
70
+ buildFilters: async (query) => buildFilters(query),
71
+ },
72
+ actions: {
73
+ create: {
74
+ commandId: 'terminal_tracking.terminal_config.create',
75
+ schema: rawBodySchema,
76
+ mapInput: async ({ raw, ctx }) => {
77
+ const { translate } = await resolveTranslations()
78
+ const scoped = withScopedPayload(raw ?? {}, ctx, translate)
79
+ return terminalConfigCreateSchema.parse(scoped)
80
+ },
81
+ response: ({ result }) => ({ id: result?.id ?? null }),
82
+ status: 201,
83
+ },
84
+ update: {
85
+ commandId: 'terminal_tracking.terminal_config.update',
86
+ schema: rawBodySchema,
87
+ mapInput: async ({ raw, ctx }) => {
88
+ const { translate } = await resolveTranslations()
89
+ const scoped = withScopedPayload(raw ?? {}, ctx, translate)
90
+ return terminalConfigUpdateSchema.parse(scoped)
91
+ },
92
+ response: ({ result }) => ({ id: result?.id ?? null }),
93
+ status: 200,
94
+ },
95
+ delete: {
96
+ commandId: 'terminal_tracking.terminal_config.delete',
97
+ schema: rawBodySchema,
98
+ mapInput: async ({ raw, ctx }) => {
99
+ const { translate } = await resolveTranslations()
100
+ const body = (raw as any)?.body ?? raw ?? {}
101
+ const scoped = withScopedPayload(body, ctx, translate)
102
+ return { id: body.id, ...scoped }
103
+ },
104
+ response: ({ result }) => ({ id: result?.id ?? null }),
105
+ status: 200,
106
+ },
107
+ },
108
+ })
109
+
110
+ export const openApi = createTerminalTrackingCrudOpenApi({
111
+ resourceName: 'TerminalConfig',
112
+ pluralName: 'TerminalConfigs',
113
+ querySchema: terminalConfigListSchema,
114
+ listResponseSchema: createPagedListResponseSchema(
115
+ z.object({
116
+ id: z.string().uuid(),
117
+ terminalCode: z.string(),
118
+ adapterType: z.string(),
119
+ displayName: z.string(),
120
+ isActive: z.boolean(),
121
+ }),
122
+ ),
123
+ create: { schema: rawBodySchema, description: 'Creates a new terminal configuration.' },
124
+ update: {
125
+ schema: rawBodySchema,
126
+ responseSchema: defaultOkResponseSchema,
127
+ description: 'Updates an existing terminal configuration.',
128
+ },
129
+ del: {
130
+ schema: z.object({ id: z.string().uuid() }),
131
+ responseSchema: defaultOkResponseSchema,
132
+ description: 'Deletes a terminal configuration.',
133
+ },
134
+ })
135
+
136
+ export const metadata = crud.metadata
137
+ export const GET = crud.GET
138
+ export const POST = crud.POST
139
+ export const PUT = crud.PUT
140
+ export const DELETE = crud.DELETE
@@ -0,0 +1,64 @@
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
6
+ import type { CommandBus } from '@open-mercato/shared/lib/commands'
7
+
8
+ export const metadata = {
9
+ POST: { requireAuth: true, requireFeatures: ['terminal_tracking.terminal_configs.manage'] },
10
+ }
11
+
12
+ const testSchema = z.object({ id: z.string().uuid() })
13
+
14
+ export async function POST(request: NextRequest) {
15
+ const auth = await getAuthFromRequest(request)
16
+ if (!auth || !auth.orgId || !auth.tenantId) {
17
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
18
+ }
19
+
20
+ let body: unknown
21
+ try {
22
+ body = await request.json()
23
+ } catch {
24
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
25
+ }
26
+
27
+ const parsed = testSchema.safeParse(body)
28
+ if (!parsed.success) {
29
+ return NextResponse.json({ error: 'Invalid request', details: parsed.error.flatten() }, { status: 400 })
30
+ }
31
+
32
+ const container = await createRequestContainer()
33
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request })
34
+ const commandBus = container.resolve('commandBus') as CommandBus
35
+ const selectedOrgId = scope?.selectedId ?? auth.orgId
36
+ const tenantId = auth.tenantId
37
+
38
+ try {
39
+ const { result } = await commandBus.execute<
40
+ { id: string; tenantId: string; organizationId: string },
41
+ { success: boolean; message: string; latencyMs?: number }
42
+ >('terminal_tracking.terminal_config.test', {
43
+ input: { id: parsed.data.id, tenantId, organizationId: selectedOrgId },
44
+ ctx: {
45
+ container,
46
+ auth,
47
+ organizationScope: scope,
48
+ selectedOrganizationId: selectedOrgId ?? null,
49
+ organizationIds: scope?.filterIds ?? (selectedOrgId ? [selectedOrgId] : null),
50
+ request,
51
+ },
52
+ metadata: {
53
+ tenantId: tenantId ?? null,
54
+ organizationId: selectedOrgId ?? null,
55
+ resourceKind: 'terminal_tracking.terminal_config',
56
+ resourceId: parsed.data.id,
57
+ },
58
+ })
59
+ return NextResponse.json(result)
60
+ } catch (error) {
61
+ const message = error instanceof Error ? error.message : 'Failed to test terminal config'
62
+ return NextResponse.json({ error: message }, { status: 400 })
63
+ }
64
+ }
@@ -0,0 +1,116 @@
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import type { EntityManager } from '@mikro-orm/postgresql'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
6
+ import { TerminalTrackingJob, TerminalEvent, TerminalVesselVisit } from '../../../data/entities'
7
+
8
+ export const metadata = {
9
+ GET: { requireAuth: true, requireFeatures: ['terminal_tracking.tracking_jobs.view'] },
10
+ }
11
+
12
+ export async function GET(request: NextRequest) {
13
+ const auth = await getAuthFromRequest(request)
14
+ if (!auth || !auth.orgId || !auth.tenantId) {
15
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
16
+ }
17
+
18
+ const id = new URL(request.url).searchParams.get('id')
19
+ if (!id) {
20
+ return NextResponse.json({ error: 'Missing id' }, { status: 400 })
21
+ }
22
+
23
+ const container = await createRequestContainer()
24
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request })
25
+ const em = container.resolve('em') as EntityManager
26
+ const orgIds = scope?.filterIds ?? [auth.orgId]
27
+
28
+ const job = await em.findOne(TerminalTrackingJob, {
29
+ id,
30
+ tenantId: auth.tenantId,
31
+ organizationId: { $in: orgIds },
32
+ deletedAt: null,
33
+ })
34
+ if (!job) {
35
+ return NextResponse.json({ error: 'Not found' }, { status: 404 })
36
+ }
37
+
38
+ const events = await em.find(
39
+ TerminalEvent,
40
+ { job: job.id, tenantId: auth.tenantId },
41
+ { orderBy: { eventDateTime: 'asc' } },
42
+ )
43
+
44
+ // Resolved vessel visits for this container's legs, keyed by visit ref.
45
+ const visitRefs = [
46
+ ...new Set(
47
+ events.flatMap((e) => [e.visitRefIn, e.visitRefOut]).filter((r): r is string => !!r),
48
+ ),
49
+ ]
50
+ const visits = visitRefs.length
51
+ ? await em.find(TerminalVesselVisit, {
52
+ tenantId: auth.tenantId,
53
+ organizationId: { $in: orgIds },
54
+ terminalCode: job.terminalCode,
55
+ visitRef: { $in: visitRefs },
56
+ })
57
+ : []
58
+ const vesselVisits = Object.fromEntries(
59
+ visits.map((v) => [
60
+ v.visitRef,
61
+ {
62
+ visitRef: v.visitRef,
63
+ vesselName: v.vesselName ?? null,
64
+ ibVoyage: v.ibVoyage ?? null,
65
+ obVoyage: v.obVoyage ?? null,
66
+ line: v.line ?? null,
67
+ phase: v.phase ?? null,
68
+ eta: v.eta ? v.eta.toISOString() : null,
69
+ etd: v.etd ? v.etd.toISOString() : null,
70
+ ata: v.ata ? v.ata.toISOString() : null,
71
+ atd: v.atd ? v.atd.toISOString() : null,
72
+ beginReceive: v.beginReceive ? v.beginReceive.toISOString() : null,
73
+ dryCutoff: v.dryCutoff ? v.dryCutoff.toISOString() : null,
74
+ },
75
+ ]),
76
+ )
77
+
78
+ return NextResponse.json({
79
+ job: {
80
+ id: job.id,
81
+ terminalCode: job.terminalCode,
82
+ containerNumber: job.containerNumber,
83
+ status: job.status,
84
+ lastPollAt: job.lastPollAt ? job.lastPollAt.toISOString() : null,
85
+ nextPollAt: job.nextPollAt ? job.nextPollAt.toISOString() : null,
86
+ retryCount: job.retryCount,
87
+ errorHistory: job.errorHistory ?? [],
88
+ createdAt: job.createdAt ? job.createdAt.toISOString() : null,
89
+ },
90
+ events: events.map((e) => ({
91
+ id: e.id,
92
+ ufvGkey: e.ufvGkey,
93
+ sourceEventId: e.sourceEventId,
94
+ eventType: e.eventType,
95
+ eventCode: e.eventCode,
96
+ eventClassifierCode: e.eventClassifierCode ?? null,
97
+ eventDateTime: e.eventDateTime ? e.eventDateTime.toISOString() : null,
98
+ transitState: e.transitState ?? null,
99
+ visitState: e.visitState ?? null,
100
+ facilityCode: e.facilityCode ?? null,
101
+ facilityCodeListProvider: e.facilityCodeListProvider ?? null,
102
+ unlocode: e.unlocode ?? null,
103
+ visitRefIn: e.visitRefIn ?? null,
104
+ visitRefOut: e.visitRefOut ?? null,
105
+ vesselName: e.vesselName ?? null,
106
+ voyageNumber: e.voyageNumber ?? null,
107
+ modeOfTransport: e.modeOfTransport ?? null,
108
+ seals: e.seals ?? null,
109
+ vgmWeightKg: e.vgmWeightKg ?? null,
110
+ impediments: e.impediments ?? null,
111
+ loadedAt: e.loadedAt ? e.loadedAt.toISOString() : null,
112
+ rawData: e.rawData ?? null,
113
+ })),
114
+ vesselVisits,
115
+ })
116
+ }
@@ -0,0 +1,64 @@
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
6
+ import type { CommandBus } from '@open-mercato/shared/lib/commands'
7
+
8
+ export const metadata = {
9
+ POST: { requireAuth: true, requireFeatures: ['terminal_tracking.tracking_jobs.manage'] },
10
+ }
11
+
12
+ const pollSchema = z.object({ id: z.string().uuid() })
13
+
14
+ export async function POST(request: NextRequest) {
15
+ const auth = await getAuthFromRequest(request)
16
+ if (!auth || !auth.orgId || !auth.tenantId) {
17
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
18
+ }
19
+
20
+ let body: unknown
21
+ try {
22
+ body = await request.json()
23
+ } catch {
24
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
25
+ }
26
+
27
+ const parsed = pollSchema.safeParse(body)
28
+ if (!parsed.success) {
29
+ return NextResponse.json({ error: 'Invalid request', details: parsed.error.flatten() }, { status: 400 })
30
+ }
31
+
32
+ const container = await createRequestContainer()
33
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request })
34
+ const commandBus = container.resolve('commandBus') as CommandBus
35
+ const selectedOrgId = scope?.selectedId ?? auth.orgId
36
+ const tenantId = auth.tenantId
37
+
38
+ try {
39
+ const { result } = await commandBus.execute<
40
+ { jobId: string; tenantId: string; organizationId: string },
41
+ { newEvents: number; updatedEvents: number }
42
+ >('terminal_tracking.tracking.poll_single', {
43
+ input: { jobId: parsed.data.id, tenantId, organizationId: selectedOrgId },
44
+ ctx: {
45
+ container,
46
+ auth,
47
+ organizationScope: scope,
48
+ selectedOrganizationId: selectedOrgId ?? null,
49
+ organizationIds: scope?.filterIds ?? (selectedOrgId ? [selectedOrgId] : null),
50
+ request,
51
+ },
52
+ metadata: {
53
+ tenantId: tenantId ?? null,
54
+ organizationId: selectedOrgId ?? null,
55
+ resourceKind: 'terminal_tracking.tracking_job',
56
+ resourceId: parsed.data.id,
57
+ },
58
+ })
59
+ return NextResponse.json(result)
60
+ } catch (error) {
61
+ const message = error instanceof Error ? error.message : 'Failed to poll terminal tracking job'
62
+ return NextResponse.json({ error: message }, { status: 400 })
63
+ }
64
+ }