@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,219 @@
1
+ import type {
2
+ TerminalAdapter,
3
+ TerminalFetchResult,
4
+ TerminalFetchedEvent,
5
+ TerminalAdapterTestResult,
6
+ ResolvedTerminalConfig,
7
+ NormalizedVesselVisit,
8
+ } from '../../terminal-adapter'
9
+ import type { IncosContainer, IncosContainerResponse, IncosVesselVisitResponse } from './types'
10
+ import { basicAuthHeader, BctAuthError } from './auth/basic'
11
+ import { mapContainerToEvents, normalizeIncosVesselVisit, splitVisitRef } from './incos-semantics'
12
+ import { n4Request } from '../n4/http'
13
+ import { terminalLogger } from '../../logger'
14
+
15
+ function joinUrl(base: string, path: string): string {
16
+ return `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
17
+ }
18
+
19
+ /**
20
+ * Config-driven adapter for the INCOS API that fronts BCT (Bałtycki Terminal
21
+ * Kontenerowy, Gdynia). One instance serves every INCOS terminal configured with
22
+ * `adapterType: 'bct'`; per-terminal differences live in the TerminalConfig.
23
+ *
24
+ * Differences from the N4 adapter that shape this code:
25
+ * - Auth is plain HTTP Basic (see ./auth/basic.ts), not an OAuth2 Bearer grant —
26
+ * there is no token endpoint or cache; the header is rebuilt per request.
27
+ * - The container lookup is a per-container path GET (`/rest-container/container/
28
+ * {nbr}`) returning a single snapshot, so `fetchEventsBatch` loops
29
+ * `fetchEvents` (bounded upstream by the per-terminal limiter), like GCT.
30
+ * - INCOS keys a vessel visit on (vessel, voyage), so the container mapping packs
31
+ * both into the visit ref (`CODE/VOY`) and `fetchVesselVisit` splits it back;
32
+ * enrichment runs only when the TerminalConfig sets `endpoints.vessel`.
33
+ */
34
+ export class BctTerminalAdapter implements TerminalAdapter {
35
+ readonly adapterType = 'bct'
36
+
37
+ readonly defaultEndpoints = {
38
+ // INCOS's container-lookup base occupies the generic `unit` slot; the
39
+ // container number is appended as a path segment per request.
40
+ unit: '/rest-container/container',
41
+ // Vessel-visit lookup base; vesselCode + voyage are appended per request.
42
+ // Enrichment only runs when the TerminalConfig also sets `endpoints.vessel`
43
+ // (the service gates on it) — this is the path used when it does.
44
+ vessel: '/rest-vesselvisit/vesselvisit',
45
+ }
46
+
47
+ private containerUrl(containerNumber: string, config: ResolvedTerminalConfig): string {
48
+ const path = config.endpoints?.unit?.trim() || this.defaultEndpoints.unit
49
+ return `${joinUrl(config.baseUrl, path)}/${encodeURIComponent(containerNumber)}`
50
+ }
51
+
52
+ private vesselUrl(vesselCode: string, voyage: string, config: ResolvedTerminalConfig): string {
53
+ const path = config.endpoints?.vessel?.trim() || this.defaultEndpoints.vessel
54
+ return `${joinUrl(config.baseUrl, path)}/${encodeURIComponent(vesselCode)}/${encodeURIComponent(voyage)}`
55
+ }
56
+
57
+ private async getContainer(
58
+ containerNumber: string,
59
+ config: ResolvedTerminalConfig,
60
+ ): Promise<IncosContainer | null> {
61
+ const { status, ok, text } = await n4Request(
62
+ this.containerUrl(containerNumber, config),
63
+ {
64
+ method: 'GET',
65
+ headers: {
66
+ Authorization: basicAuthHeader(config),
67
+ Accept: 'application/json',
68
+ },
69
+ },
70
+ { proxyUrl: config.proxyUrl, label: `${config.terminalCode} container` },
71
+ )
72
+
73
+ if (status === 401 || status === 403) {
74
+ throw new BctAuthError(`INCOS container lookup unauthorized (${status}) for ${config.terminalCode}`)
75
+ }
76
+ if (!ok) {
77
+ throw new Error(`INCOS container lookup failed (${status}) for ${config.terminalCode}`)
78
+ }
79
+
80
+ let parsed: IncosContainerResponse
81
+ try {
82
+ parsed = JSON.parse(text) as IncosContainerResponse
83
+ } catch {
84
+ throw new Error(`INCOS container response was not valid JSON for ${config.terminalCode}`)
85
+ }
86
+
87
+ // An unknown container does NOT error: INCOS echoes it back as a SUCCESS
88
+ // envelope with the number filled in and every other field null (observed
89
+ // live) — so the timestamp-gating in `mapContainerToEvents` yields no events
90
+ // and the job keeps polling. A genuine ERROR envelope (bad request etc.) is
91
+ // treated the same "not found → no events" way rather than thrown, so one odd
92
+ // response can't sink a poll. (Bad credentials surface earlier as a 401.)
93
+ if (String(parsed.status ?? '').toUpperCase() !== 'SUCCESS') return null
94
+ const data = parsed.data
95
+ if (!data || !data.container_nbr) return null
96
+ return data
97
+ }
98
+
99
+ async fetchEvents(input: {
100
+ containerNumber: string
101
+ config: ResolvedTerminalConfig
102
+ }): Promise<TerminalFetchResult> {
103
+ const { containerNumber, config } = input
104
+ const container = await this.getContainer(containerNumber, config)
105
+ const events = container ? mapContainerToEvents(container, config) : []
106
+
107
+ terminalLogger.debug('bct fetchEvents', {
108
+ terminalCode: config.terminalCode,
109
+ container: containerNumber,
110
+ found: Boolean(container),
111
+ events: events.length,
112
+ })
113
+
114
+ return { events, containerNumber }
115
+ }
116
+
117
+ /**
118
+ * INCOS has no batch-by-container-number call, so fetch each container in turn.
119
+ * Kept as a distinct method (rather than falling through to per-container in
120
+ * the service) so the scheduler's batch path stays a single call site; the
121
+ * per-terminal rate limiter still throttles the underlying requests. A single
122
+ * container's failure is isolated so one bad number can't sink the whole poll —
123
+ * except an auth error, which is fatal to the batch (bad credentials would just
124
+ * repeat for every container).
125
+ */
126
+ async fetchEventsBatch(input: {
127
+ containerNumbers: string[]
128
+ config: ResolvedTerminalConfig
129
+ }): Promise<{ events: TerminalFetchedEvent[] }> {
130
+ const { containerNumbers, config } = input
131
+ if (containerNumbers.length === 0) return { events: [] }
132
+
133
+ const events: TerminalFetchedEvent[] = []
134
+ for (const containerNumber of containerNumbers) {
135
+ try {
136
+ const result = await this.fetchEvents({ containerNumber, config })
137
+ events.push(...result.events)
138
+ } catch (err) {
139
+ if (err instanceof BctAuthError) throw err
140
+ terminalLogger.warn('bct fetchEventsBatch: container failed', {
141
+ terminalCode: config.terminalCode,
142
+ container: containerNumber,
143
+ message: err instanceof Error ? err.message : String(err),
144
+ })
145
+ }
146
+ }
147
+ return { events }
148
+ }
149
+
150
+ /**
151
+ * Resolve a vessel visit for a `CODE/VOY` ref (as produced by the container
152
+ * mapping's `visitRefIn`/`visitRefOut`). INCOS keys visits by (vesselCode,
153
+ * voyage), so the ref is split back into the two path segments. Returns null
154
+ * when the ref is malformed or INCOS reports no such visit (or its per-caller
155
+ * vessel-lookup rate limit is hit — an ERROR envelope). Vessel-visit dates are
156
+ * `YYYY-MM-DD HH:MM:SS`, parsed by `normalizeIncosVesselVisit`.
157
+ */
158
+ async fetchVesselVisit(input: {
159
+ visitRef: string
160
+ config: ResolvedTerminalConfig
161
+ }): Promise<NormalizedVesselVisit | null> {
162
+ const { visitRef, config } = input
163
+ const parts = splitVisitRef(visitRef)
164
+ if (!parts) return null
165
+
166
+ const { status, ok, text } = await n4Request(
167
+ this.vesselUrl(parts.vesselCode, parts.voyage, config),
168
+ { method: 'GET', headers: { Authorization: basicAuthHeader(config), Accept: 'application/json' } },
169
+ { proxyUrl: config.proxyUrl, label: `${config.terminalCode} vesselvisit` },
170
+ )
171
+
172
+ if (status === 401 || status === 403) {
173
+ throw new BctAuthError(`INCOS vessel lookup unauthorized (${status}) for ${config.terminalCode}`)
174
+ }
175
+ if (!ok) {
176
+ throw new Error(`INCOS vessel lookup failed (${status}) for ${config.terminalCode}`)
177
+ }
178
+
179
+ let parsed: IncosVesselVisitResponse
180
+ try {
181
+ parsed = JSON.parse(text) as IncosVesselVisitResponse
182
+ } catch {
183
+ throw new Error(`INCOS vessel response was not valid JSON for ${config.terminalCode}`)
184
+ }
185
+ if (String(parsed.status ?? '').toUpperCase() !== 'SUCCESS' || !parsed.data) return null
186
+ return normalizeIncosVesselVisit(parsed.data, visitRef)
187
+ }
188
+
189
+ /**
190
+ * INCOS has no ping/health route, so probe the container endpoint with a
191
+ * placeholder number. Verified live: a wrong password returns HTTP 401, while
192
+ * an unknown container returns HTTP 200 with a SUCCESS envelope of nulls — so
193
+ * a 401/403 means the Basic credentials are wrong and any other outcome proves
194
+ * the host is reachable and the credentials are accepted. Missing credentials
195
+ * surface via `basicAuthHeader` throwing before any request.
196
+ */
197
+ async testConnection(config: ResolvedTerminalConfig): Promise<TerminalAdapterTestResult> {
198
+ const started = Date.now()
199
+ try {
200
+ const probe = 'TEST0000000'
201
+ const { status } = await n4Request(
202
+ this.containerUrl(probe, config),
203
+ { method: 'GET', headers: { Authorization: basicAuthHeader(config), Accept: 'application/json' } },
204
+ { proxyUrl: config.proxyUrl, label: `${config.terminalCode} test`, retries: 0 },
205
+ )
206
+ if (status === 401 || status === 403) {
207
+ return {
208
+ success: false,
209
+ message: `Authentication rejected (${status}) — check username/password`,
210
+ latencyMs: Date.now() - started,
211
+ }
212
+ }
213
+ return { success: true, message: `Reachable and authenticated (HTTP ${status})`, latencyMs: Date.now() - started }
214
+ } catch (err) {
215
+ const message = err instanceof Error ? err.message : String(err)
216
+ return { success: false, message, latencyMs: Date.now() - started }
217
+ }
218
+ }
219
+ }
@@ -0,0 +1,34 @@
1
+ import type { ResolvedTerminalConfig } from '../../../terminal-adapter'
2
+
3
+ /**
4
+ * INCOS authenticates every call with plain HTTP Basic (`-u user:password` in
5
+ * the vendor docs). There is no token endpoint or cache — the header is derived
6
+ * from the decrypted `auth_config` on each request. Credentials live in
7
+ * `auth_config` as `{ username, password }` (encrypted at rest; see
8
+ * encryption.ts).
9
+ */
10
+
11
+ export class BctAuthError extends Error {
12
+ constructor(message: string) {
13
+ super(message)
14
+ this.name = 'BctAuthError'
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Build the `Authorization: Basic <base64(user:pass)>` header for a config.
20
+ * Throws `BctAuthError` when either credential is missing, so a misconfigured
21
+ * terminal fails with a clear message rather than a silent 401.
22
+ */
23
+ export function basicAuthHeader(config: ResolvedTerminalConfig): string {
24
+ const auth = (config.authConfig ?? {}) as Record<string, unknown>
25
+ const username = String(auth.username ?? '')
26
+ const password = String(auth.password ?? '')
27
+ if (!username || !password) {
28
+ throw new BctAuthError(
29
+ `BCT/INCOS config ${config.terminalCode} is missing username/password in auth_config`,
30
+ )
31
+ }
32
+ const token = Buffer.from(`${username}:${password}`, 'utf8').toString('base64')
33
+ return `Basic ${token}`
34
+ }
@@ -0,0 +1,291 @@
1
+ import type { IncosContainer, IncosVesselVisit } from './types'
2
+ import { DEFAULT_TERMINAL_TIMEZONE, zonedNaiveToUtc } from '../zoned-time'
3
+ import type {
4
+ TerminalFetchedEvent,
5
+ ResolvedTerminalConfig,
6
+ NormalizedVesselVisit,
7
+ } from '../../terminal-adapter'
8
+ import type {
9
+ TerminalEventType,
10
+ TerminalEventClassifierCode,
11
+ TerminalModeOfTransport,
12
+ } from '../../../data/entities'
13
+
14
+ /**
15
+ * INCOS returns a container *state snapshot*, not a timestamped milestone stream
16
+ * (much like GCT). We emit only the container's CURRENT milestone — its arrival
17
+ * (`in_yard_date`) while it is on the terminal, or its departure
18
+ * (`out_yard_date`) once it has left — mirroring N4's `/unit` current-state row,
19
+ * rather than synthesizing extra states we cannot timestamp. INCOS has no
20
+ * separate loaded-onto-transport timestamp (N4's "Loaded" column), so no `LOAD`
21
+ * event.
22
+ *
23
+ * The arrival is refined by `in_yard_type` to match N4's vocabulary: a vessel
24
+ * arrival is a discharge (`DISC` → `equipment.discharged`), a truck/rail arrival
25
+ * is a gate-in (`GTIN` → `equipment.gate_in`). The departure is `DEPA`
26
+ * (`transport.departed`). All three `eventCode`s are the ones the service maps
27
+ * via n4-semantics `semanticEventIdFor`, so no adapter-specific downstream
28
+ * wiring is needed.
29
+ */
30
+
31
+ type MilestoneSpec = {
32
+ eventType: TerminalEventType
33
+ eventCode: string
34
+ classifier: TerminalEventClassifierCode
35
+ }
36
+
37
+ const GATE_IN: MilestoneSpec = { eventType: 'EQUIPMENT', eventCode: 'GTIN', classifier: 'ACT' }
38
+ const DISCHARGED: MilestoneSpec = { eventType: 'EQUIPMENT', eventCode: 'DISC', classifier: 'ACT' }
39
+ const DEPARTED: MilestoneSpec = { eventType: 'TRANSPORT', eventCode: 'DEPA', classifier: 'ACT' }
40
+
41
+ /** The container's direction of travel. */
42
+ export type IncosCargoDirection = 'import' | 'export'
43
+
44
+ /**
45
+ * Map INCOS `category` to a direction. Unlike GCT (which infers from a status
46
+ * prefix), INCOS states it explicitly: `E` = export, `I`/`X` = import. Blank or
47
+ * unknown yields null.
48
+ */
49
+ export function directionFromCategory(category: string | null | undefined): IncosCargoDirection | null {
50
+ const c = (category ?? '').trim().toUpperCase()
51
+ if (!c) return null
52
+ if (c === 'E') return 'export'
53
+ if (c === 'I' || c === 'X') return 'import'
54
+ return null
55
+ }
56
+
57
+ /**
58
+ * Map an INCOS yard-move type (`in_yard_type` / `out_yard_type`) to a mode of
59
+ * transport: `T` truck, `V` vessel, `R` rail. Blank/unknown yields null.
60
+ */
61
+ export function modeFromYardType(type: string | null | undefined): TerminalModeOfTransport | null {
62
+ const t = (type ?? '').trim().toUpperCase()
63
+ if (t === 'T') return 'TRUCK'
64
+ if (t === 'V') return 'VESSEL'
65
+ if (t === 'R') return 'RAIL'
66
+ return null
67
+ }
68
+
69
+ /**
70
+ * Parse an INCOS datetime — `DD-MM-YYYY HH:MI` (optionally with seconds), e.g.
71
+ * `03-05-2020 03:28`. INCOS returns terminal-local time without an offset, so we
72
+ * interpret it in the terminal's timezone (Europe/Warsaw by default) and return
73
+ * the corresponding UTC instant. Returns null when empty or the shape does not
74
+ * match (a strict match avoids `Date.parse` misreading the day-first order as
75
+ * month-first).
76
+ */
77
+ export function parseIncosDateTime(
78
+ value: string | null | undefined,
79
+ timeZone: string = DEFAULT_TERMINAL_TIMEZONE,
80
+ ): Date | null {
81
+ if (!value) return null
82
+ const m = /^(\d{2})-(\d{2})-(\d{4})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/.exec(value.trim())
83
+ if (!m) return null
84
+ const [, dd, mm, yyyy, hh, mi, ss] = m
85
+ return zonedNaiveToUtc(+yyyy, +mm, +dd, +hh, +mi, ss ? +ss : 0, timeZone)
86
+ }
87
+
88
+ /**
89
+ * Parse an INCOS **vessel-visit** datetime — `YYYY-MM-DD HH:MM[:SS]`, e.g.
90
+ * `2026-08-13 18:00:00`. This is a DIFFERENT format from the container endpoint's
91
+ * day-first `DD-MM-YYYY HH:MI` (confirmed against the live API). Terminal-local,
92
+ * no offset → interpreted in the terminal's timezone, consistent with
93
+ * `parseIncosDateTime`.
94
+ */
95
+ export function parseIncosVesselDateTime(
96
+ value: string | null | undefined,
97
+ timeZone: string = DEFAULT_TERMINAL_TIMEZONE,
98
+ ): Date | null {
99
+ if (!value) return null
100
+ const m = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/.exec(value.trim())
101
+ if (!m) return null
102
+ const [, yyyy, mm, dd, hh, mi, ss] = m
103
+ return zonedNaiveToUtc(+yyyy, +mm, +dd, +hh, +mi, ss ? +ss : 0, timeZone)
104
+ }
105
+
106
+ /**
107
+ * Encode an INCOS vessel visit key. INCOS looks a visit up by (vesselCode,
108
+ * voyage), so both are packed into one visit ref as `CODE/VOY` (vessel codes and
109
+ * voyages contain no `/`). Returns null unless both parts are present.
110
+ */
111
+ export function combinedVisitRef(
112
+ code: string | null | undefined,
113
+ voyage: string | null | undefined,
114
+ ): string | null {
115
+ const c = (code ?? '').trim()
116
+ const v = (voyage ?? '').trim()
117
+ return c && v ? `${c}/${v}` : null
118
+ }
119
+
120
+ /** Split a `CODE/VOY` visit ref back into its parts (on the first `/`). */
121
+ export function splitVisitRef(visitRef: string): { vesselCode: string; voyage: string } | null {
122
+ const i = visitRef.indexOf('/')
123
+ if (i <= 0 || i >= visitRef.length - 1) return null
124
+ return { vesselCode: visitRef.slice(0, i), voyage: visitRef.slice(i + 1) }
125
+ }
126
+
127
+ /**
128
+ * Normalize an INCOS vessel-visit payload into the shared `NormalizedVesselVisit`.
129
+ * INCOS has no `phase`, `beginReceive`, or `dryCutoff`, so those are null.
130
+ */
131
+ export function normalizeIncosVesselVisit(
132
+ data: IncosVesselVisit,
133
+ visitRef: string,
134
+ ): NormalizedVesselVisit {
135
+ return {
136
+ visitRef,
137
+ vesselName: data.vessel_name ?? null,
138
+ ibVoyage: data.in_voy ?? null,
139
+ obVoyage: data.out_voy ?? null,
140
+ line: data.line_code ?? null,
141
+ phase: null,
142
+ eta: parseIncosVesselDateTime(data.eta),
143
+ etd: parseIncosVesselDateTime(data.etd),
144
+ ata: parseIncosVesselDateTime(data.ata),
145
+ atd: parseIncosVesselDateTime(data.atd),
146
+ beginReceive: null,
147
+ dryCutoff: null,
148
+ rawData: { ...data },
149
+ }
150
+ }
151
+
152
+ /** Split a `;`-separated INCOS list, dropping blanks. */
153
+ function splitList(value: string | null | undefined): string[] {
154
+ if (!value) return []
155
+ return value
156
+ .split(';')
157
+ .map((s) => s.trim())
158
+ .filter((s) => s !== '')
159
+ }
160
+
161
+ function buildSourceEventId(terminalCode: string, ufvGkey: string, eventCode: string): string {
162
+ return `${terminalCode}:${ufvGkey}:${eventCode}`
163
+ }
164
+
165
+ /** Per-milestone view of the snapshot: which yard-type + vessel fields apply. */
166
+ type MilestoneContext = {
167
+ yardType: string | null | undefined
168
+ vesselName: string | null | undefined
169
+ voyage: string | null | undefined
170
+ }
171
+
172
+ function baseEvent(
173
+ container: IncosContainer,
174
+ config: ResolvedTerminalConfig,
175
+ ufvGkey: string,
176
+ spec: MilestoneSpec,
177
+ eventDateTime: Date,
178
+ ctx: MilestoneContext,
179
+ ): TerminalFetchedEvent {
180
+ const seals = [container.seal1, container.seal2, container.seal3, container.seal4]
181
+ .filter((n): n is string => typeof n === 'string' && n.trim() !== '')
182
+ .map((number) => ({ number, source: config.terminalCode }))
183
+
184
+ const direction = directionFromCategory(container.category)
185
+
186
+ // Impediments ("stopki"). A customs HOLD is mapped to the direction-specific
187
+ // catalogue code (`CUSTOMS IMPORT/EXPORT HOLD`) so `lib/holds.ts` classifies it
188
+ // as a critical customs hold instead of the "unknown" fallback. Per the INCOS
189
+ // docs `customs_status` is binary — empty, or `HOLD` when a customs hold is
190
+ // placed ("założony") — so this is the full fidelity INCOS offers: it is a real
191
+ // placed hold (not N4's routine auto-clearing "permission"), hence `critical`.
192
+ // Direction unknown → the import code (the dominant BCT flow). Raw `constraint`
193
+ // codes pass through and stay unclassified until INCOS documents them.
194
+ const customsHold =
195
+ String(container.customs_status ?? '').trim().toUpperCase() === 'HOLD'
196
+ ? [direction === 'export' ? 'CUSTOMS EXPORT HOLD' : 'CUSTOMS IMPORT HOLD']
197
+ : []
198
+ const impediments = [...customsHold, ...splitList(container.constraint)]
199
+
200
+ const vgm = typeof container.vgm_weight === 'number' ? container.vgm_weight : null
201
+
202
+ return {
203
+ source: 'terminal',
204
+ sourceEventId: buildSourceEventId(config.terminalCode, ufvGkey, spec.eventCode),
205
+ eventType: spec.eventType,
206
+ eventCode: spec.eventCode,
207
+ eventClassifierCode: spec.classifier,
208
+ eventDateTime,
209
+ // `ufvGkey` is the trimmed container number — reuse it so `containerNumber`
210
+ // and the dedup key can never disagree (and to avoid a raw-field cast).
211
+ containerNumber: ufvGkey,
212
+ ufvGkey,
213
+ // No S-code state on INCOS; the location is the closest transit signal.
214
+ transitState: container.actual_location ?? null,
215
+ visitState: null,
216
+ facilityCode: config.facilityCode ?? null,
217
+ facilityCodeListProvider: config.facilityCodeListProvider ?? null,
218
+ unlocode: config.unlocode ?? null,
219
+ // INCOS keys a vessel visit by (vesselCode, voyage), so encode both into the
220
+ // visit ref (`CODE/VOY`); the adapter's fetchVesselVisit splits it back apart.
221
+ visitRefIn: combinedVisitRef(container.vessel_code_in, container.vessel_visit_invoy),
222
+ visitRefOut: combinedVisitRef(container.vessel_code_out, container.vessel_visit_outvoy),
223
+ // Vessel name/voyage here are the container-snapshot values; when the config
224
+ // enables the vessel endpoint the service overwrites them (and adds ETA/ATA)
225
+ // from the resolved vessel visit.
226
+ vesselName: ctx.vesselName ?? null,
227
+ voyageNumber: ctx.voyage ?? null,
228
+ modeOfTransport: modeFromYardType(ctx.yardType),
229
+ seals: seals.length ? seals : null,
230
+ vgmWeightKg: vgm,
231
+ impediments: impediments.length ? impediments : null,
232
+ loadedAt: null,
233
+ // Preserve the full snapshot. `Category` (import/export) is the field
234
+ // `parseCargoCategory` reads to pick the vessel-visit side for enrichment.
235
+ rawData: { ...container, Category: direction ?? null },
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Turn one INCOS container snapshot into zero, one, or two normalized events:
241
+ * a gate-in when `in_yard_date` is set, and a departed when `out_yard_date` is
242
+ * set. A snapshot with neither timestamp yields no events (nothing has provably
243
+ * happened yet). The dedup key is the container number — the container lookup
244
+ * carries no per-visit gkey, so (unlike GCT's `VisitNo`) a container that
245
+ * returns for a later visit reuses the same key.
246
+ */
247
+ export function mapContainerToEvents(
248
+ container: IncosContainer,
249
+ config: ResolvedTerminalConfig,
250
+ ): TerminalFetchedEvent[] {
251
+ const containerNbr = (container.container_nbr ?? '').trim()
252
+ if (!containerNbr) return []
253
+ const ufvGkey = containerNbr
254
+
255
+ // Emit only the container's CURRENT milestone, mirroring N4's `/unit` (which
256
+ // returns a single row for the current transit state):
257
+ // - departed (out_yard_date set) → just DEPA;
258
+ // - still on terminal (in_yard_date set, no out) → its arrival (DISC/GTIN).
259
+ // Emitting BOTH the historical arrival and the departure on every poll would
260
+ // keep the job from ever completing — the service's `isFullyDeparted` requires
261
+ // *every* event in a poll to be DEPA — so a departed container would be polled
262
+ // forever, wasting requests against BCT's 10k/day cap. The arrival was already
263
+ // persisted (deduped by sourceEventId) while the box sat in the yard, so it
264
+ // stays on the timeline; only its redundant re-emission is dropped here.
265
+ const departed = parseIncosDateTime(container.out_yard_date)
266
+ if (departed) {
267
+ return [
268
+ baseEvent(container, config, ufvGkey, DEPARTED, departed, {
269
+ yardType: container.out_yard_type,
270
+ vesselName: container.vessel_name_out,
271
+ voyage: container.vessel_visit_outvoy,
272
+ }),
273
+ ]
274
+ }
275
+
276
+ const grounded = parseIncosDateTime(container.in_yard_date)
277
+ if (grounded) {
278
+ // A vessel arrival is a discharge; a truck/rail (or unspecified) arrival is a
279
+ // gate-in — matching how N4 splits DISC vs GTIN.
280
+ const arrivalSpec = modeFromYardType(container.in_yard_type) === 'VESSEL' ? DISCHARGED : GATE_IN
281
+ return [
282
+ baseEvent(container, config, ufvGkey, arrivalSpec, grounded, {
283
+ yardType: container.in_yard_type,
284
+ vesselName: container.vessel_name_in,
285
+ voyage: container.vessel_visit_invoy,
286
+ }),
287
+ ]
288
+ }
289
+
290
+ return []
291
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Response shapes for the INCOS API that fronts BCT (Bałtycki Terminal
3
+ * Kontenerowy, Gdynia). INCOS is a plain REST/JSON platform on `https://incos.pl`
4
+ * — unrelated to Navis N4 or the GCT FastAPI. Only the fields Phase 1
5
+ * (read-only container tracking) consumes are typed here; the full container
6
+ * payload is preserved verbatim into `rawData`.
7
+ *
8
+ * Source: "dokumentacja api INCOS" (BCT_INCOS_API_1, rev. 01.03.2021),
9
+ * endpoint "Sprawdź kontener" — `GET /rest-container/container/{nbr}`.
10
+ */
11
+
12
+ /**
13
+ * One container as returned inside the `data` member of the container lookup.
14
+ * INCOS emits every field, using `null` for what it has no value for. Dates are
15
+ * `DD-MM-YYYY HH:MI` in terminal-local time (see `parseIncosDateTime`).
16
+ */
17
+ export type IncosContainer = {
18
+ /** Container number. */
19
+ container_nbr?: string | null
20
+ /** ISO type code, e.g. `22G1`. */
21
+ container_iso_type?: string | null
22
+ /**
23
+ * Current location — free-form terminal text, inconsistent between the doc and
24
+ * the live API: observed values include `Y` (yard) and `OFF DOCK` (the vendor
25
+ * doc listed YARD/OFFDOCK/VESSEL/CFS). Stored verbatim into `transitState`;
26
+ * nothing branches on it.
27
+ */
28
+ actual_location?: string | null
29
+ /** Load status: E empty / F full. */
30
+ status?: string | null
31
+ /** Relation/category: E export / I,X import. */
32
+ category?: string | null
33
+ /** Shipping line code. */
34
+ line_code?: string | null
35
+ seal1?: string | null
36
+ seal2?: string | null
37
+ seal3?: string | null
38
+ seal4?: string | null
39
+ temp_min?: string | number | null
40
+ temp_max?: string | number | null
41
+ temp_set?: string | number | null
42
+ /** EU status Y/N. */
43
+ eu_status?: string | null
44
+ /** Customs hold: empty, or `HOLD` when a customs hold is set. */
45
+ customs_status?: string | null
46
+ /** Other constraints, a `;`-separated list (e.g. `STOPUC`). */
47
+ constraint?: string | null
48
+ /** IMO classes, `;`-separated. */
49
+ imo_class?: string | null
50
+ /** UN codes, `;`-separated (aligned to the imo_class order). */
51
+ un_code?: string | null
52
+ vgm_weight?: number | null
53
+ gross_weight?: number | null
54
+ net_weight?: number | null
55
+ /** Entry (gate-in / discharge) instant — `DD-MM-YYYY HH:MI`. */
56
+ in_yard_date?: string | null
57
+ /** How it entered: T truck / V vessel / R rail. */
58
+ in_yard_type?: string | null
59
+ /** Exit (gate-out / departure) instant — `DD-MM-YYYY HH:MI`. */
60
+ out_yard_date?: string | null
61
+ /** How it left: T truck / V vessel / R rail. */
62
+ out_yard_type?: string | null
63
+ vessel_code_in?: string | null
64
+ vessel_name_in?: string | null
65
+ vessel_visit_invoy?: string | null
66
+ vessel_code_out?: string | null
67
+ vessel_name_out?: string | null
68
+ vessel_visit_outvoy?: string | null
69
+ /** Whether a pre-advice already exists for the container in the terminal (Y/N). */
70
+ prelodge?: string | null
71
+ // Remaining fields are preserved via the index signature into rawData.
72
+ [key: string]: unknown
73
+ }
74
+
75
+ /**
76
+ * Vessel-visit data from `GET /rest-vesselvisit/vesselvisit/{code}/{voyage}`.
77
+ * NOTE: the live API returns dates as `YYYY-MM-DD HH:MM:SS` (terminal-local, no
78
+ * offset) — a DIFFERENT format from the container endpoint's `DD-MM-YYYY HH:MI`.
79
+ */
80
+ export type IncosVesselVisit = {
81
+ vessel_code?: string | null
82
+ vessel_name?: string | null
83
+ in_voy?: string | null
84
+ out_voy?: string | null
85
+ line_code?: string | null
86
+ eta?: string | null
87
+ etd?: string | null
88
+ ata?: string | null
89
+ atd?: string | null
90
+ [key: string]: unknown
91
+ }
92
+
93
+ /** `GET /rest-vesselvisit/vesselvisit/{code}/{voyage}` response body. */
94
+ export type IncosVesselVisitResponse = {
95
+ data?: IncosVesselVisit | null
96
+ status?: string | null
97
+ }
98
+
99
+ /**
100
+ * The INCOS envelope shared by reads and writes. For the container lookup,
101
+ * `data` is a single container object (not an array); `status` is `SUCCESS` on
102
+ * a hit and `ERROR` otherwise (the payload then carries an `error` block).
103
+ */
104
+ export type IncosContainerResponse = {
105
+ data?: IncosContainer | null
106
+ status?: string | null
107
+ error?: {
108
+ type?: number | null
109
+ message?: string | null
110
+ fieldErrList?: string | null
111
+ fieldName?: string | null
112
+ } | null
113
+ }