@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.
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/generated/entities/terminal_config/index.js +53 -0
- package/dist/generated/entities/terminal_config/index.js.map +7 -0
- package/dist/generated/entities/terminal_event/index.js +59 -0
- package/dist/generated/entities/terminal_event/index.js.map +7 -0
- package/dist/generated/entities/terminal_tracking_job/index.js +37 -0
- package/dist/generated/entities/terminal_tracking_job/index.js.map +7 -0
- package/dist/generated/entities/terminal_vessel_visit/index.js +41 -0
- package/dist/generated/entities/terminal_vessel_visit/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +26 -0
- package/dist/generated/entities.ids.generated.js.map +7 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/terminal_tracking/acl.js +14 -0
- package/dist/modules/terminal_tracking/acl.js.map +7 -0
- package/dist/modules/terminal_tracking/api/openapi.js +27 -0
- package/dist/modules/terminal_tracking/api/openapi.js.map +7 -0
- package/dist/modules/terminal_tracking/api/terminal-configs/route.js +140 -0
- package/dist/modules/terminal_tracking/api/terminal-configs/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/terminal-configs/test/route.js +58 -0
- package/dist/modules/terminal_tracking/api/terminal-configs/test/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/details/route.js +109 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/details/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/poll/route.js +58 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/poll/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/route.js +189 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/utils.js +12 -0
- package/dist/modules/terminal_tracking/api/utils.js.map +7 -0
- package/dist/modules/terminal_tracking/api/vessel-visits/route.js +83 -0
- package/dist/modules/terminal_tracking/api/vessel-visits/route.js.map +7 -0
- package/dist/modules/terminal_tracking/backend/terminal-configs/page.js +669 -0
- package/dist/modules/terminal_tracking/backend/terminal-configs/page.js.map +7 -0
- package/dist/modules/terminal_tracking/backend/terminal-configs/page.meta.js +19 -0
- package/dist/modules/terminal_tracking/backend/terminal-configs/page.meta.js.map +7 -0
- package/dist/modules/terminal_tracking/backend/terminal-containers/page.js +275 -0
- package/dist/modules/terminal_tracking/backend/terminal-containers/page.js.map +7 -0
- package/dist/modules/terminal_tracking/backend/terminal-containers/page.meta.js +19 -0
- package/dist/modules/terminal_tracking/backend/terminal-containers/page.meta.js.map +7 -0
- package/dist/modules/terminal_tracking/ce.js +35 -0
- package/dist/modules/terminal_tracking/ce.js.map +7 -0
- package/dist/modules/terminal_tracking/commands/index.js +4 -0
- package/dist/modules/terminal_tracking/commands/index.js.map +7 -0
- package/dist/modules/terminal_tracking/commands/terminal-configs.js +107 -0
- package/dist/modules/terminal_tracking/commands/terminal-configs.js.map +7 -0
- package/dist/modules/terminal_tracking/commands/terminal-tracking.js +27 -0
- package/dist/modules/terminal_tracking/commands/terminal-tracking.js.map +7 -0
- package/dist/modules/terminal_tracking/commands/tracking-jobs.js +56 -0
- package/dist/modules/terminal_tracking/commands/tracking-jobs.js.map +7 -0
- package/dist/modules/terminal_tracking/components/ContainerDetailsDrawer.js +371 -0
- package/dist/modules/terminal_tracking/components/ContainerDetailsDrawer.js.map +7 -0
- package/dist/modules/terminal_tracking/data/entities.js +337 -0
- package/dist/modules/terminal_tracking/data/entities.js.map +7 -0
- package/dist/modules/terminal_tracking/data/validators.js +98 -0
- package/dist/modules/terminal_tracking/data/validators.js.map +7 -0
- package/dist/modules/terminal_tracking/di.js +39 -0
- package/dist/modules/terminal_tracking/di.js.map +7 -0
- package/dist/modules/terminal_tracking/encryption.js +14 -0
- package/dist/modules/terminal_tracking/encryption.js.map +7 -0
- package/dist/modules/terminal_tracking/events.js +45 -0
- package/dist/modules/terminal_tracking/events.js.map +7 -0
- package/dist/modules/terminal_tracking/i18n/de.json +175 -0
- package/dist/modules/terminal_tracking/i18n/en.json +178 -0
- package/dist/modules/terminal_tracking/i18n/es.json +175 -0
- package/dist/modules/terminal_tracking/i18n/pl.json +206 -0
- package/dist/modules/terminal_tracking/index.js +15 -0
- package/dist/modules/terminal_tracking/index.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/adapter.js +164 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/adapter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/auth/basic.js +23 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/auth/basic.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/incos-semantics.js +146 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/incos-semantics.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/types.js +1 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/types.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/adapter.js +106 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/adapter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/auth/token.js +120 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/auth/token.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/gct-semantics.js +77 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/gct-semantics.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/types.js +1 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/types.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/index.js +12 -0
- package/dist/modules/terminal_tracking/lib/adapters/index.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/adapter.js +121 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/adapter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/auth/ropc.js +81 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/auth/ropc.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/http.js +66 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/http.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/n4-semantics.js +95 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/n4-semantics.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.js +70 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.js +44 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/proxy.js +15 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/proxy.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/types.js +1 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/types.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/zoned-time.js +27 -0
- package/dist/modules/terminal_tracking/lib/adapters/zoned-time.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/availability.js +40 -0
- package/dist/modules/terminal_tracking/lib/availability.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/ensure-schedule.js +51 -0
- package/dist/modules/terminal_tracking/lib/ensure-schedule.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/holds.js +240 -0
- package/dist/modules/terminal_tracking/lib/holds.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/logger.js +19 -0
- package/dist/modules/terminal_tracking/lib/logger.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/rate-limiter.js +30 -0
- package/dist/modules/terminal_tracking/lib/rate-limiter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/stops.js +48 -0
- package/dist/modules/terminal_tracking/lib/stops.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/terminal-adapter.js +1 -0
- package/dist/modules/terminal_tracking/lib/terminal-adapter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/totp.js +41 -0
- package/dist/modules/terminal_tracking/lib/totp.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.js +101 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.js +39 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.js +13 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.js +15 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.js +13 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.js.map +7 -0
- package/dist/modules/terminal_tracking/services/terminalMatcherService.js +61 -0
- package/dist/modules/terminal_tracking/services/terminalMatcherService.js.map +7 -0
- package/dist/modules/terminal_tracking/services/terminalRegistry.js +24 -0
- package/dist/modules/terminal_tracking/services/terminalRegistry.js.map +7 -0
- package/dist/modules/terminal_tracking/services/terminalTrackingService.js +687 -0
- package/dist/modules/terminal_tracking/services/terminalTrackingService.js.map +7 -0
- package/dist/modules/terminal_tracking/setup.js +23 -0
- package/dist/modules/terminal_tracking/setup.js.map +7 -0
- package/dist/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.js +22 -0
- package/dist/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.js.map +7 -0
- package/generated/entities/terminal_config/index.ts +36 -0
- package/generated/entities/terminal_event/index.ts +39 -0
- package/generated/entities/terminal_tracking_job/index.ts +28 -0
- package/generated/entities/terminal_vessel_visit/index.ts +30 -0
- package/generated/entities.ids.generated.ts +52 -0
- package/package.json +80 -0
- package/src/index.ts +44 -0
- package/src/modules/terminal_tracking/__integration__/TC-TRACK-301-gct-config-test-connection.spec.ts +66 -0
- package/src/modules/terminal_tracking/__integration__/TC-TRACK-317-bct-config-test-connection.spec.ts +68 -0
- package/src/modules/terminal_tracking/__integration__/helpers.ts +148 -0
- package/src/modules/terminal_tracking/__integration__/meta.ts +4 -0
- package/src/modules/terminal_tracking/acl.ts +10 -0
- package/src/modules/terminal_tracking/api/openapi.ts +28 -0
- package/src/modules/terminal_tracking/api/terminal-configs/route.ts +140 -0
- package/src/modules/terminal_tracking/api/terminal-configs/test/route.ts +64 -0
- package/src/modules/terminal_tracking/api/tracking-jobs/details/route.ts +116 -0
- package/src/modules/terminal_tracking/api/tracking-jobs/poll/route.ts +64 -0
- package/src/modules/terminal_tracking/api/tracking-jobs/route.ts +243 -0
- package/src/modules/terminal_tracking/api/utils.ts +10 -0
- package/src/modules/terminal_tracking/api/vessel-visits/route.ts +95 -0
- package/src/modules/terminal_tracking/backend/terminal-configs/page.meta.ts +16 -0
- package/src/modules/terminal_tracking/backend/terminal-configs/page.tsx +728 -0
- package/src/modules/terminal_tracking/backend/terminal-containers/page.meta.ts +16 -0
- package/src/modules/terminal_tracking/backend/terminal-containers/page.tsx +328 -0
- package/src/modules/terminal_tracking/ce.ts +31 -0
- package/src/modules/terminal_tracking/commands/index.ts +3 -0
- package/src/modules/terminal_tracking/commands/terminal-configs.ts +132 -0
- package/src/modules/terminal_tracking/commands/terminal-tracking.ts +34 -0
- package/src/modules/terminal_tracking/commands/tracking-jobs.ts +69 -0
- package/src/modules/terminal_tracking/components/ContainerDetailsDrawer.tsx +540 -0
- package/src/modules/terminal_tracking/data/entities.ts +386 -0
- package/src/modules/terminal_tracking/data/validators.ts +113 -0
- package/src/modules/terminal_tracking/di.ts +46 -0
- package/src/modules/terminal_tracking/encryption.ts +12 -0
- package/src/modules/terminal_tracking/events.ts +48 -0
- package/src/modules/terminal_tracking/i18n/de.json +175 -0
- package/src/modules/terminal_tracking/i18n/en.json +178 -0
- package/src/modules/terminal_tracking/i18n/es.json +175 -0
- package/src/modules/terminal_tracking/i18n/pl.json +206 -0
- package/src/modules/terminal_tracking/index.ts +13 -0
- package/src/modules/terminal_tracking/lib/__tests__/availability.test.ts +136 -0
- package/src/modules/terminal_tracking/lib/__tests__/ensure-schedule.test.ts +67 -0
- package/src/modules/terminal_tracking/lib/__tests__/holds.test.ts +73 -0
- package/src/modules/terminal_tracking/lib/__tests__/stops.test.ts +80 -0
- package/src/modules/terminal_tracking/lib/__tests__/totp.test.ts +38 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/__tests__/adapter.test.ts +196 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/__tests__/incos-semantics.test.ts +303 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/adapter.ts +219 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/auth/basic.ts +34 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/incos-semantics.ts +291 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/types.ts +113 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/__tests__/adapter.test.ts +238 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/__tests__/gct-semantics.test.ts +133 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/adapter.ts +152 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/auth/token.ts +219 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/gct-semantics.ts +136 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/types.ts +58 -0
- package/src/modules/terminal_tracking/lib/adapters/index.ts +16 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/adapter-proxy.test.ts +69 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/data-table.test.ts +179 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/http.test.ts +98 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/n4-semantics.test.ts +140 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/proxy.test.ts +19 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/vessel-data-table.test.ts +67 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/adapter.ts +163 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/auth/ropc.ts +104 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/http.ts +127 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/n4-semantics.ts +156 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.ts +102 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.ts +60 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/proxy.ts +24 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/types.ts +56 -0
- package/src/modules/terminal_tracking/lib/adapters/zoned-time.ts +65 -0
- package/src/modules/terminal_tracking/lib/availability.ts +119 -0
- package/src/modules/terminal_tracking/lib/ensure-schedule.ts +83 -0
- package/src/modules/terminal_tracking/lib/holds.ts +344 -0
- package/src/modules/terminal_tracking/lib/logger.ts +21 -0
- package/src/modules/terminal_tracking/lib/rate-limiter.ts +52 -0
- package/src/modules/terminal_tracking/lib/stops.ts +124 -0
- package/src/modules/terminal_tracking/lib/terminal-adapter.ts +136 -0
- package/src/modules/terminal_tracking/lib/totp.ts +63 -0
- package/src/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.ts +104 -0
- package/src/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.ts +40 -0
- package/src/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.ts +11 -0
- package/src/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.ts +13 -0
- package/src/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.ts +16 -0
- package/src/modules/terminal_tracking/services/__tests__/availabilityEmit.test.ts +277 -0
- package/src/modules/terminal_tracking/services/__tests__/createJob.test.ts +59 -0
- package/src/modules/terminal_tracking/services/__tests__/pollAllBatch.test.ts +147 -0
- package/src/modules/terminal_tracking/services/__tests__/terminalMatcher.test.ts +70 -0
- package/src/modules/terminal_tracking/services/__tests__/vesselResolution.test.ts +453 -0
- package/src/modules/terminal_tracking/services/terminalMatcherService.ts +97 -0
- package/src/modules/terminal_tracking/services/terminalRegistry.ts +30 -0
- package/src/modules/terminal_tracking/services/terminalTrackingService.ts +912 -0
- package/src/modules/terminal_tracking/setup.ts +24 -0
- 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
|
+
}
|