@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,196 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest'
2
+
3
+ // The adapter's fetches run through `n4Request`, which drives undici's `fetch`.
4
+ // Mock the undici module, keeping the real ProxyAgent/Agent so the dispatcher is
5
+ // built exactly as in production.
6
+ const { fetchMock } = vi.hoisted(() => ({ fetchMock: vi.fn() }))
7
+ vi.mock('undici', async (orig) => {
8
+ const actual = await orig<typeof import('undici')>()
9
+ return { ...actual, fetch: fetchMock }
10
+ })
11
+
12
+ import { BctTerminalAdapter } from '../adapter'
13
+ import type { ResolvedTerminalConfig } from '../../../terminal-adapter'
14
+
15
+ function cfg(overrides: Partial<ResolvedTerminalConfig> = {}): ResolvedTerminalConfig {
16
+ return {
17
+ terminalCode: 'bct',
18
+ adapterType: 'bct',
19
+ displayName: 'BCT',
20
+ baseUrl: 'https://incos.pl',
21
+ proxyUrl: null,
22
+ endpoints: { unit: '/rest-container/container' },
23
+ authType: 'basic',
24
+ authConfig: { username: 'user', password: 'secret' },
25
+ rateLimitRequests: 60,
26
+ rateLimitWindowSeconds: 60,
27
+ unlocode: 'PLBCT',
28
+ ...overrides,
29
+ }
30
+ }
31
+
32
+ function successResponse(data: Record<string, unknown>) {
33
+ return new Response(JSON.stringify({ data, status: 'SUCCESS' }), { status: 200 })
34
+ }
35
+ function errorResponse() {
36
+ return new Response(JSON.stringify({ error: { type: 0, message: 'not found' }, status: 'ERROR' }), { status: 200 })
37
+ }
38
+
39
+ const EXPECTED_BASIC = `Basic ${Buffer.from('user:secret', 'utf8').toString('base64')}`
40
+
41
+ afterEach(() => fetchMock.mockReset())
42
+
43
+ describe('BctTerminalAdapter.fetchEvents', () => {
44
+ // TC-TRACK-315 (mapping + Authorization header form + URL)
45
+ it('sends HTTP Basic, hits the container path, and maps the snapshot', async () => {
46
+ fetchMock.mockResolvedValueOnce(
47
+ successResponse({
48
+ container_nbr: 'MEDU1221231',
49
+ in_yard_date: '03-05-2020 03:28',
50
+ in_yard_type: 'T',
51
+ category: 'I',
52
+ }),
53
+ )
54
+
55
+ const { events } = await new BctTerminalAdapter().fetchEvents({
56
+ containerNumber: 'MEDU1221231',
57
+ config: cfg(),
58
+ })
59
+
60
+ expect(events).toHaveLength(1)
61
+ expect(events[0].eventCode).toBe('GTIN')
62
+ expect(events[0].sourceEventId).toBe('bct:MEDU1221231:GTIN')
63
+
64
+ const call = fetchMock.mock.calls[0]!
65
+ expect(String(call[0])).toBe('https://incos.pl/rest-container/container/MEDU1221231')
66
+ expect((call[1] as any).method).toBe('GET')
67
+ expect((call[1] as any).headers.Authorization).toBe(EXPECTED_BASIC)
68
+ })
69
+
70
+ it('returns [] for an INCOS ERROR envelope (container not found) without throwing', async () => {
71
+ fetchMock.mockResolvedValueOnce(errorResponse())
72
+ const { events } = await new BctTerminalAdapter().fetchEvents({ containerNumber: 'X', config: cfg() })
73
+ expect(events).toEqual([])
74
+ })
75
+
76
+ it('throws BctAuthError on a 401', async () => {
77
+ fetchMock.mockResolvedValueOnce(new Response('unauthorized', { status: 401 }))
78
+ await expect(
79
+ new BctTerminalAdapter().fetchEvents({ containerNumber: 'X', config: cfg() }),
80
+ ).rejects.toThrow(/unauthorized/)
81
+ })
82
+
83
+ it('throws on a non-JSON body', async () => {
84
+ fetchMock.mockResolvedValueOnce(new Response('<html>500</html>', { status: 200 }))
85
+ await expect(
86
+ new BctTerminalAdapter().fetchEvents({ containerNumber: 'X', config: cfg() }),
87
+ ).rejects.toThrow(/valid JSON/)
88
+ })
89
+
90
+ it('fails when auth_config is missing credentials, before any request', async () => {
91
+ await expect(
92
+ new BctTerminalAdapter().fetchEvents({ containerNumber: 'X', config: cfg({ authConfig: {} }) }),
93
+ ).rejects.toThrow(/username\/password/)
94
+ expect(fetchMock).not.toHaveBeenCalled()
95
+ })
96
+ })
97
+
98
+ describe('BctTerminalAdapter.fetchEventsBatch', () => {
99
+ it('loops per container and isolates a single container failure', async () => {
100
+ // container A: 500 → isolated; container B: SUCCESS with a mappable row
101
+ fetchMock.mockResolvedValueOnce(new Response('err', { status: 500 }))
102
+ fetchMock.mockResolvedValueOnce(
103
+ successResponse({ container_nbr: 'B', in_yard_date: '03-05-2020 03:28' }),
104
+ )
105
+
106
+ const { events } = await new BctTerminalAdapter().fetchEventsBatch({
107
+ containerNumbers: ['A', 'B'],
108
+ config: cfg(),
109
+ })
110
+ expect(events).toHaveLength(1)
111
+ expect(events[0].containerNumber).toBe('B')
112
+ })
113
+
114
+ it('fails the whole batch on an auth error (bad credentials repeat for every container)', async () => {
115
+ fetchMock.mockResolvedValue(new Response('nope', { status: 403 }))
116
+ await expect(
117
+ new BctTerminalAdapter().fetchEventsBatch({ containerNumbers: ['A', 'B'], config: cfg() }),
118
+ ).rejects.toThrow()
119
+ })
120
+ })
121
+
122
+ describe('BctTerminalAdapter.testConnection', () => {
123
+ // TC-TRACK-316
124
+ it('reports success when the probe is reachable and authenticated (ERROR envelope, HTTP 200)', async () => {
125
+ fetchMock.mockResolvedValueOnce(errorResponse())
126
+ const res = await new BctTerminalAdapter().testConnection(cfg())
127
+ expect(res.success).toBe(true)
128
+ expect(fetchMock.mock.calls[0]![1]).toMatchObject({ method: 'GET' })
129
+ expect((fetchMock.mock.calls[0]![1] as any).headers.Authorization).toBe(EXPECTED_BASIC)
130
+ })
131
+
132
+ it('reports failure on a 401/403 without leaking the password', async () => {
133
+ fetchMock.mockResolvedValueOnce(new Response('nope', { status: 401 }))
134
+ const res = await new BctTerminalAdapter().testConnection(cfg())
135
+ expect(res.success).toBe(false)
136
+ expect(res.message).toContain('401')
137
+ expect(res.message).not.toContain('secret')
138
+ })
139
+
140
+ it('fails clearly when auth_config is missing credentials', async () => {
141
+ const res = await new BctTerminalAdapter().testConnection(cfg({ authConfig: {} }))
142
+ expect(res.success).toBe(false)
143
+ expect(res.message).toMatch(/username\/password/)
144
+ expect(fetchMock).not.toHaveBeenCalled()
145
+ })
146
+ })
147
+
148
+ describe('BctTerminalAdapter.fetchVesselVisit', () => {
149
+ const vesselCfg = () => cfg({ endpoints: { unit: '/rest-container/container', vessel: '/rest-vesselvisit/vesselvisit' } })
150
+ function vesselResponse(data: Record<string, unknown>) {
151
+ return new Response(JSON.stringify({ data, status: 'SUCCESS' }), { status: 200 })
152
+ }
153
+
154
+ // TC-TRACK-321: splits CODE/VOY into the path and maps the visit.
155
+ it('GETs {base}/{code}/{voyage} with Basic auth and normalizes the visit', async () => {
156
+ fetchMock.mockResolvedValueOnce(
157
+ vesselResponse({
158
+ vessel_code: 'MSADMIR',
159
+ vessel_name: 'ADMIRAL NEPTUNE (MSC)',
160
+ in_voy: 'EO630R',
161
+ out_voy: 'EO630R',
162
+ line_code: 'MSC',
163
+ eta: '2026-08-13 18:00:00',
164
+ ata: '2026-08-13 18:55:00',
165
+ }),
166
+ )
167
+ const visit = await new BctTerminalAdapter().fetchVesselVisit({ visitRef: 'MSADMIR/EO630R', config: vesselCfg() })
168
+ expect(visit).not.toBeNull()
169
+ expect(visit!.vesselName).toBe('ADMIRAL NEPTUNE (MSC)')
170
+ // 18:00 terminal-local (Europe/Warsaw, CEST +02:00) → 16:00 UTC.
171
+ expect(visit!.eta?.toISOString()).toBe('2026-08-13T16:00:00.000Z')
172
+
173
+ const call = fetchMock.mock.calls[0]!
174
+ expect(String(call[0])).toBe('https://incos.pl/rest-vesselvisit/vesselvisit/MSADMIR/EO630R')
175
+ expect((call[1] as any).headers.Authorization).toBe(EXPECTED_BASIC)
176
+ })
177
+
178
+ it('returns null for a malformed ref without any request', async () => {
179
+ const visit = await new BctTerminalAdapter().fetchVesselVisit({ visitRef: 'EO630R', config: vesselCfg() })
180
+ expect(visit).toBeNull()
181
+ expect(fetchMock).not.toHaveBeenCalled()
182
+ })
183
+
184
+ it('returns null on an ERROR envelope (unknown visit / rate limit)', async () => {
185
+ fetchMock.mockResolvedValueOnce(errorResponse())
186
+ const visit = await new BctTerminalAdapter().fetchVesselVisit({ visitRef: 'MSADMIR/EO630R', config: vesselCfg() })
187
+ expect(visit).toBeNull()
188
+ })
189
+
190
+ it('throws BctAuthError on a 401', async () => {
191
+ fetchMock.mockResolvedValueOnce(new Response('nope', { status: 401 }))
192
+ await expect(
193
+ new BctTerminalAdapter().fetchVesselVisit({ visitRef: 'MSADMIR/EO630R', config: vesselCfg() }),
194
+ ).rejects.toThrow(/unauthorized/)
195
+ })
196
+ })
@@ -0,0 +1,303 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ mapContainerToEvents,
4
+ directionFromCategory,
5
+ modeFromYardType,
6
+ parseIncosDateTime,
7
+ parseIncosVesselDateTime,
8
+ combinedVisitRef,
9
+ splitVisitRef,
10
+ normalizeIncosVesselVisit,
11
+ } from '../incos-semantics'
12
+ import type { IncosContainer } from '../types'
13
+ import type { ResolvedTerminalConfig } from '../../../terminal-adapter'
14
+ import { isKnownHoldCode, getHoldInfo } from '../../../holds'
15
+
16
+ const config: ResolvedTerminalConfig = {
17
+ terminalCode: 'bct',
18
+ adapterType: 'bct',
19
+ displayName: 'Bałtycki Terminal Kontenerowy',
20
+ baseUrl: 'https://incos.pl',
21
+ endpoints: { unit: '/rest-container/container' },
22
+ authType: 'basic',
23
+ rateLimitRequests: 60,
24
+ rateLimitWindowSeconds: 60,
25
+ unlocode: 'PLBCT',
26
+ }
27
+
28
+ function container(partial: Partial<IncosContainer>): IncosContainer {
29
+ return { container_nbr: 'MEDU1221231', ...partial }
30
+ }
31
+
32
+ describe('directionFromCategory', () => {
33
+ it('maps E to export and I/X to import', () => {
34
+ expect(directionFromCategory('E')).toBe('export')
35
+ expect(directionFromCategory('I')).toBe('import')
36
+ expect(directionFromCategory('X')).toBe('import')
37
+ expect(directionFromCategory('e')).toBe('export')
38
+ })
39
+ it('returns null for blank/unknown', () => {
40
+ expect(directionFromCategory('')).toBeNull()
41
+ expect(directionFromCategory('Z')).toBeNull()
42
+ expect(directionFromCategory(null)).toBeNull()
43
+ })
44
+ })
45
+
46
+ describe('modeFromYardType', () => {
47
+ it('maps T/V/R to TRUCK/VESSEL/RAIL', () => {
48
+ expect(modeFromYardType('T')).toBe('TRUCK')
49
+ expect(modeFromYardType('V')).toBe('VESSEL')
50
+ expect(modeFromYardType('R')).toBe('RAIL')
51
+ })
52
+ it('returns null for blank/unknown', () => {
53
+ expect(modeFromYardType('')).toBeNull()
54
+ expect(modeFromYardType('Q')).toBeNull()
55
+ expect(modeFromYardType(null)).toBeNull()
56
+ })
57
+ })
58
+
59
+ describe('parseIncosDateTime', () => {
60
+ it('parses DD-MM-YYYY HH:MI as terminal-local → UTC (day-first, not month-first)', () => {
61
+ // 03:28 CEST (+02:00) on 2020-05-03 is 01:28 UTC.
62
+ expect(parseIncosDateTime('03-05-2020 03:28')?.toISOString()).toBe('2020-05-03T01:28:00.000Z')
63
+ // 13 could not be a month — proves day-first parsing.
64
+ expect(parseIncosDateTime('13-07-2020 10:00')?.toISOString()).toBe('2020-07-13T08:00:00.000Z')
65
+ })
66
+ it('tolerates an optional seconds component', () => {
67
+ expect(parseIncosDateTime('03-05-2020 03:28:45')?.toISOString()).toBe('2020-05-03T01:28:45.000Z')
68
+ })
69
+ it('returns null for empty/garbage/ISO-shaped input', () => {
70
+ expect(parseIncosDateTime(null)).toBeNull()
71
+ expect(parseIncosDateTime(' ')).toBeNull()
72
+ expect(parseIncosDateTime('not-a-date')).toBeNull()
73
+ expect(parseIncosDateTime('2020-05-03T03:28:00Z')).toBeNull()
74
+ })
75
+ })
76
+
77
+ describe('mapContainerToEvents', () => {
78
+ // TC-TRACK-311
79
+ it('emits one gate_in (GTIN) from in_yard_date, keyed on container_nbr', () => {
80
+ const events = mapContainerToEvents(
81
+ container({ in_yard_date: '03-05-2020 03:28', in_yard_type: 'T', category: 'I', vgm_weight: 24000 }),
82
+ config,
83
+ )
84
+ expect(events).toHaveLength(1)
85
+ const ev = events[0]
86
+ expect(ev.eventType).toBe('EQUIPMENT')
87
+ expect(ev.eventCode).toBe('GTIN')
88
+ expect(ev.eventClassifierCode).toBe('ACT')
89
+ expect(ev.ufvGkey).toBe('MEDU1221231')
90
+ expect(ev.sourceEventId).toBe('bct:MEDU1221231:GTIN')
91
+ expect(ev.containerNumber).toBe('MEDU1221231')
92
+ expect(ev.unlocode).toBe('PLBCT')
93
+ expect(ev.vgmWeightKg).toBe(24000)
94
+ expect(ev.modeOfTransport).toBe('TRUCK')
95
+ expect(ev.eventDateTime.toISOString()).toBe('2020-05-03T01:28:00.000Z')
96
+ // `Category` (not `direction`) so the service's parseCargoCategory picks the vessel side.
97
+ expect((ev.rawData as Record<string, unknown>).Category).toBe('import')
98
+ })
99
+
100
+ // TC-TRACK-318
101
+ it('emits a discharge (DISC) — not gate_in — when the arrival is by vessel', () => {
102
+ const events = mapContainerToEvents(
103
+ container({ in_yard_date: '14-08-2026 04:03', in_yard_type: 'V', category: 'I' }),
104
+ config,
105
+ )
106
+ expect(events).toHaveLength(1)
107
+ expect(events[0].eventType).toBe('EQUIPMENT')
108
+ expect(events[0].eventCode).toBe('DISC')
109
+ expect(events[0].sourceEventId).toBe('bct:MEDU1221231:DISC')
110
+ expect(events[0].modeOfTransport).toBe('VESSEL')
111
+ })
112
+
113
+ it('emits gate_in (GTIN) when the arrival is by rail (R), like truck', () => {
114
+ const events = mapContainerToEvents(
115
+ container({ in_yard_date: '14-08-2026 04:03', in_yard_type: 'R' }),
116
+ config,
117
+ )
118
+ expect(events[0].eventCode).toBe('GTIN')
119
+ expect(events[0].modeOfTransport).toBe('RAIL')
120
+ })
121
+
122
+ // TC-TRACK-312
123
+ it('emits ONLY DEPA (current state) once departed, dropping the historical arrival', () => {
124
+ const events = mapContainerToEvents(
125
+ container({
126
+ in_yard_date: '03-05-2020 03:28',
127
+ in_yard_type: 'T',
128
+ out_yard_date: '05-05-2020 14:00',
129
+ out_yard_type: 'V',
130
+ vessel_name_out: 'AILA',
131
+ vessel_visit_outvoy: '66-1E',
132
+ }),
133
+ config,
134
+ )
135
+ // Not [GTIN, DEPA] — the arrival was already persisted while in-yard; keeping
136
+ // it here would stop the job from ever reaching 'completed'.
137
+ expect(events.map((e) => e.eventCode)).toEqual(['DEPA'])
138
+ const dep = events[0]
139
+ expect(dep.eventType).toBe('TRANSPORT')
140
+ expect(dep.sourceEventId).toBe('bct:MEDU1221231:DEPA')
141
+ expect(dep.eventDateTime.toISOString()).toBe('2020-05-05T12:00:00.000Z')
142
+ expect(dep.modeOfTransport).toBe('VESSEL')
143
+ expect(dep.vesselName).toBe('AILA')
144
+ expect(dep.voyageNumber).toBe('66-1E')
145
+ })
146
+
147
+ it('emits DEPA from out_yard_date even when no in_yard_date was ever seen', () => {
148
+ const events = mapContainerToEvents(
149
+ container({ out_yard_date: '05-05-2020 14:00', out_yard_type: 'T' }),
150
+ config,
151
+ )
152
+ expect(events.map((e) => e.eventCode)).toEqual(['DEPA'])
153
+ expect(events[0].modeOfTransport).toBe('TRUCK')
154
+ })
155
+
156
+ it('emits no events when neither yard timestamp is present', () => {
157
+ expect(mapContainerToEvents(container({ status: 'E', category: 'E' }), config)).toEqual([])
158
+ })
159
+
160
+ // TC-TRACK-313
161
+ it('maps a customs HOLD to the direction-specific catalogue code + passes constraint codes through', () => {
162
+ const imp = mapContainerToEvents(
163
+ container({ in_yard_date: '03-05-2020 03:28', category: 'I', customs_status: 'HOLD', constraint: 'STOPUC; STOPLINE ;' }),
164
+ config,
165
+ )
166
+ expect(imp[0].impediments).toEqual(['CUSTOMS IMPORT HOLD', 'STOPUC', 'STOPLINE'])
167
+
168
+ const exp = mapContainerToEvents(
169
+ container({ in_yard_date: '03-05-2020 03:28', category: 'E', customs_status: 'HOLD' }),
170
+ config,
171
+ )
172
+ expect(exp[0].impediments).toEqual(['CUSTOMS EXPORT HOLD'])
173
+ })
174
+
175
+ // TC-TRACK-319: the mapped customs code must resolve in the shared holds
176
+ // catalogue (critical), not fall to the "unknown" fallback.
177
+ it('emits a customs-hold code that the holds catalogue classifies as critical', () => {
178
+ const events = mapContainerToEvents(
179
+ container({ in_yard_date: '03-05-2020 03:28', category: 'I', customs_status: 'HOLD' }),
180
+ config,
181
+ )
182
+ const code = events[0].impediments![0]
183
+ expect(isKnownHoldCode(code)).toBe(true)
184
+ expect(getHoldInfo(code).severity).toBe('critical')
185
+ expect(getHoldInfo(code).unknown).toBe(false)
186
+ })
187
+
188
+ it('defaults an unknown-direction customs HOLD to the import code', () => {
189
+ const events = mapContainerToEvents(
190
+ container({ in_yard_date: '03-05-2020 03:28', customs_status: 'HOLD' }),
191
+ config,
192
+ )
193
+ expect(events[0].impediments).toEqual(['CUSTOMS IMPORT HOLD'])
194
+ })
195
+
196
+ it('omits impediments when there is no hold and no constraint', () => {
197
+ const events = mapContainerToEvents(
198
+ container({ in_yard_date: '03-05-2020 03:28', customs_status: '', constraint: null }),
199
+ config,
200
+ )
201
+ expect(events[0].impediments).toBeNull()
202
+ })
203
+
204
+ // TC-TRACK-314
205
+ it('maps seal1..seal4 to seal objects (dropping blanks) and preserves the snapshot', () => {
206
+ const events = mapContainerToEvents(
207
+ container({
208
+ in_yard_date: '03-05-2020 03:28',
209
+ in_yard_type: 'V',
210
+ seal1: '123',
211
+ seal2: ' ',
212
+ seal3: '456',
213
+ seal4: null,
214
+ actual_location: 'YARD',
215
+ vessel_code_in: 'CSAILA',
216
+ vessel_name_in: 'AILA',
217
+ vessel_visit_invoy: '66-1I',
218
+ }),
219
+ config,
220
+ )
221
+ const ev = events[0]
222
+ expect(ev.seals).toEqual([
223
+ { number: '123', source: 'bct' },
224
+ { number: '456', source: 'bct' },
225
+ ])
226
+ expect(ev.transitState).toBe('YARD')
227
+ expect(ev.vesselName).toBe('AILA')
228
+ expect(ev.voyageNumber).toBe('66-1I')
229
+ // visitRef packs vesselCode + voyage as CODE/VOY for the vessel lookup.
230
+ expect(ev.visitRefIn).toBe('CSAILA/66-1I')
231
+ expect((ev.rawData as Record<string, unknown>).container_nbr).toBe('MEDU1221231')
232
+ })
233
+
234
+ it('returns [] for a container with no container_nbr', () => {
235
+ expect(mapContainerToEvents({ container_nbr: '' } as IncosContainer, config)).toEqual([])
236
+ expect(mapContainerToEvents({ container_nbr: null } as IncosContainer, config)).toEqual([])
237
+ })
238
+ })
239
+
240
+ // ── Vessel visit (Phase 2) ──────────────────────────────────────────────────
241
+
242
+ describe('combinedVisitRef / splitVisitRef', () => {
243
+ it('packs code + voyage and splits them back', () => {
244
+ expect(combinedVisitRef('MSADMIR', 'EO630R')).toBe('MSADMIR/EO630R')
245
+ expect(splitVisitRef('MSADMIR/EO630R')).toEqual({ vesselCode: 'MSADMIR', voyage: 'EO630R' })
246
+ })
247
+ it('needs both parts, trims, and rejects malformed refs', () => {
248
+ expect(combinedVisitRef('MSADMIR', '')).toBeNull()
249
+ expect(combinedVisitRef('', 'EO630R')).toBeNull()
250
+ expect(combinedVisitRef(null, null)).toBeNull()
251
+ expect(combinedVisitRef(' MSADMIR ', ' EO630R ')).toBe('MSADMIR/EO630R')
252
+ expect(splitVisitRef('EO630R')).toBeNull()
253
+ expect(splitVisitRef('/EO630R')).toBeNull()
254
+ expect(splitVisitRef('MSADMIR/')).toBeNull()
255
+ })
256
+ })
257
+
258
+ describe('parseIncosVesselDateTime', () => {
259
+ it('parses the vessel YYYY-MM-DD HH:MM:SS form as terminal-local → UTC', () => {
260
+ // 18:00 CEST (+02:00) on 2026-08-13 is 16:00 UTC.
261
+ expect(parseIncosVesselDateTime('2026-08-13 18:00:00')?.toISOString()).toBe('2026-08-13T16:00:00.000Z')
262
+ expect(parseIncosVesselDateTime('2026-08-13 18:00')?.toISOString()).toBe('2026-08-13T16:00:00.000Z')
263
+ })
264
+ it('rejects the container day-first form and garbage', () => {
265
+ // Container uses DD-MM-YYYY — must NOT be accepted here.
266
+ expect(parseIncosVesselDateTime('13-08-2026 18:00')).toBeNull()
267
+ expect(parseIncosVesselDateTime(null)).toBeNull()
268
+ expect(parseIncosVesselDateTime('not-a-date')).toBeNull()
269
+ })
270
+ })
271
+
272
+ describe('normalizeIncosVesselVisit', () => {
273
+ // TC-TRACK-320: maps the live vessel payload to the shared NormalizedVesselVisit.
274
+ it('maps vessel fields + ETA/ATA and nulls the N4-only fields', () => {
275
+ const v = normalizeIncosVesselVisit(
276
+ {
277
+ vessel_code: 'MSADMIR',
278
+ vessel_name: 'ADMIRAL NEPTUNE (MSC)',
279
+ in_voy: 'EO630R',
280
+ out_voy: 'EO630R',
281
+ line_code: 'MSC',
282
+ eta: '2026-08-13 18:00:00',
283
+ etd: '2026-08-14 18:00:00',
284
+ ata: '2026-08-13 18:55:00',
285
+ atd: '2026-08-14 19:30:00',
286
+ },
287
+ 'MSADMIR/EO630R',
288
+ )
289
+ expect(v.visitRef).toBe('MSADMIR/EO630R')
290
+ expect(v.vesselName).toBe('ADMIRAL NEPTUNE (MSC)')
291
+ expect(v.ibVoyage).toBe('EO630R')
292
+ expect(v.obVoyage).toBe('EO630R')
293
+ expect(v.line).toBe('MSC')
294
+ // Terminal-local (Europe/Warsaw, CEST +02:00 in August) → UTC.
295
+ expect(v.eta?.toISOString()).toBe('2026-08-13T16:00:00.000Z')
296
+ expect(v.ata?.toISOString()).toBe('2026-08-13T16:55:00.000Z')
297
+ expect(v.atd?.toISOString()).toBe('2026-08-14T17:30:00.000Z')
298
+ // INCOS has none of these.
299
+ expect(v.phase).toBeNull()
300
+ expect(v.beginReceive).toBeNull()
301
+ expect(v.dryCutoff).toBeNull()
302
+ })
303
+ })