@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,146 @@
|
|
|
1
|
+
import { DEFAULT_TERMINAL_TIMEZONE, zonedNaiveToUtc } from "../zoned-time.js";
|
|
2
|
+
const GATE_IN = { eventType: "EQUIPMENT", eventCode: "GTIN", classifier: "ACT" };
|
|
3
|
+
const DISCHARGED = { eventType: "EQUIPMENT", eventCode: "DISC", classifier: "ACT" };
|
|
4
|
+
const DEPARTED = { eventType: "TRANSPORT", eventCode: "DEPA", classifier: "ACT" };
|
|
5
|
+
function directionFromCategory(category) {
|
|
6
|
+
const c = (category ?? "").trim().toUpperCase();
|
|
7
|
+
if (!c) return null;
|
|
8
|
+
if (c === "E") return "export";
|
|
9
|
+
if (c === "I" || c === "X") return "import";
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
function modeFromYardType(type) {
|
|
13
|
+
const t = (type ?? "").trim().toUpperCase();
|
|
14
|
+
if (t === "T") return "TRUCK";
|
|
15
|
+
if (t === "V") return "VESSEL";
|
|
16
|
+
if (t === "R") return "RAIL";
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function parseIncosDateTime(value, timeZone = DEFAULT_TERMINAL_TIMEZONE) {
|
|
20
|
+
if (!value) return null;
|
|
21
|
+
const m = /^(\d{2})-(\d{2})-(\d{4})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/.exec(value.trim());
|
|
22
|
+
if (!m) return null;
|
|
23
|
+
const [, dd, mm, yyyy, hh, mi, ss] = m;
|
|
24
|
+
return zonedNaiveToUtc(+yyyy, +mm, +dd, +hh, +mi, ss ? +ss : 0, timeZone);
|
|
25
|
+
}
|
|
26
|
+
function parseIncosVesselDateTime(value, timeZone = DEFAULT_TERMINAL_TIMEZONE) {
|
|
27
|
+
if (!value) return null;
|
|
28
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/.exec(value.trim());
|
|
29
|
+
if (!m) return null;
|
|
30
|
+
const [, yyyy, mm, dd, hh, mi, ss] = m;
|
|
31
|
+
return zonedNaiveToUtc(+yyyy, +mm, +dd, +hh, +mi, ss ? +ss : 0, timeZone);
|
|
32
|
+
}
|
|
33
|
+
function combinedVisitRef(code, voyage) {
|
|
34
|
+
const c = (code ?? "").trim();
|
|
35
|
+
const v = (voyage ?? "").trim();
|
|
36
|
+
return c && v ? `${c}/${v}` : null;
|
|
37
|
+
}
|
|
38
|
+
function splitVisitRef(visitRef) {
|
|
39
|
+
const i = visitRef.indexOf("/");
|
|
40
|
+
if (i <= 0 || i >= visitRef.length - 1) return null;
|
|
41
|
+
return { vesselCode: visitRef.slice(0, i), voyage: visitRef.slice(i + 1) };
|
|
42
|
+
}
|
|
43
|
+
function normalizeIncosVesselVisit(data, visitRef) {
|
|
44
|
+
return {
|
|
45
|
+
visitRef,
|
|
46
|
+
vesselName: data.vessel_name ?? null,
|
|
47
|
+
ibVoyage: data.in_voy ?? null,
|
|
48
|
+
obVoyage: data.out_voy ?? null,
|
|
49
|
+
line: data.line_code ?? null,
|
|
50
|
+
phase: null,
|
|
51
|
+
eta: parseIncosVesselDateTime(data.eta),
|
|
52
|
+
etd: parseIncosVesselDateTime(data.etd),
|
|
53
|
+
ata: parseIncosVesselDateTime(data.ata),
|
|
54
|
+
atd: parseIncosVesselDateTime(data.atd),
|
|
55
|
+
beginReceive: null,
|
|
56
|
+
dryCutoff: null,
|
|
57
|
+
rawData: { ...data }
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function splitList(value) {
|
|
61
|
+
if (!value) return [];
|
|
62
|
+
return value.split(";").map((s) => s.trim()).filter((s) => s !== "");
|
|
63
|
+
}
|
|
64
|
+
function buildSourceEventId(terminalCode, ufvGkey, eventCode) {
|
|
65
|
+
return `${terminalCode}:${ufvGkey}:${eventCode}`;
|
|
66
|
+
}
|
|
67
|
+
function baseEvent(container, config, ufvGkey, spec, eventDateTime, ctx) {
|
|
68
|
+
const seals = [container.seal1, container.seal2, container.seal3, container.seal4].filter((n) => typeof n === "string" && n.trim() !== "").map((number) => ({ number, source: config.terminalCode }));
|
|
69
|
+
const direction = directionFromCategory(container.category);
|
|
70
|
+
const customsHold = String(container.customs_status ?? "").trim().toUpperCase() === "HOLD" ? [direction === "export" ? "CUSTOMS EXPORT HOLD" : "CUSTOMS IMPORT HOLD"] : [];
|
|
71
|
+
const impediments = [...customsHold, ...splitList(container.constraint)];
|
|
72
|
+
const vgm = typeof container.vgm_weight === "number" ? container.vgm_weight : null;
|
|
73
|
+
return {
|
|
74
|
+
source: "terminal",
|
|
75
|
+
sourceEventId: buildSourceEventId(config.terminalCode, ufvGkey, spec.eventCode),
|
|
76
|
+
eventType: spec.eventType,
|
|
77
|
+
eventCode: spec.eventCode,
|
|
78
|
+
eventClassifierCode: spec.classifier,
|
|
79
|
+
eventDateTime,
|
|
80
|
+
// `ufvGkey` is the trimmed container number — reuse it so `containerNumber`
|
|
81
|
+
// and the dedup key can never disagree (and to avoid a raw-field cast).
|
|
82
|
+
containerNumber: ufvGkey,
|
|
83
|
+
ufvGkey,
|
|
84
|
+
// No S-code state on INCOS; the location is the closest transit signal.
|
|
85
|
+
transitState: container.actual_location ?? null,
|
|
86
|
+
visitState: null,
|
|
87
|
+
facilityCode: config.facilityCode ?? null,
|
|
88
|
+
facilityCodeListProvider: config.facilityCodeListProvider ?? null,
|
|
89
|
+
unlocode: config.unlocode ?? null,
|
|
90
|
+
// INCOS keys a vessel visit by (vesselCode, voyage), so encode both into the
|
|
91
|
+
// visit ref (`CODE/VOY`); the adapter's fetchVesselVisit splits it back apart.
|
|
92
|
+
visitRefIn: combinedVisitRef(container.vessel_code_in, container.vessel_visit_invoy),
|
|
93
|
+
visitRefOut: combinedVisitRef(container.vessel_code_out, container.vessel_visit_outvoy),
|
|
94
|
+
// Vessel name/voyage here are the container-snapshot values; when the config
|
|
95
|
+
// enables the vessel endpoint the service overwrites them (and adds ETA/ATA)
|
|
96
|
+
// from the resolved vessel visit.
|
|
97
|
+
vesselName: ctx.vesselName ?? null,
|
|
98
|
+
voyageNumber: ctx.voyage ?? null,
|
|
99
|
+
modeOfTransport: modeFromYardType(ctx.yardType),
|
|
100
|
+
seals: seals.length ? seals : null,
|
|
101
|
+
vgmWeightKg: vgm,
|
|
102
|
+
impediments: impediments.length ? impediments : null,
|
|
103
|
+
loadedAt: null,
|
|
104
|
+
// Preserve the full snapshot. `Category` (import/export) is the field
|
|
105
|
+
// `parseCargoCategory` reads to pick the vessel-visit side for enrichment.
|
|
106
|
+
rawData: { ...container, Category: direction ?? null }
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function mapContainerToEvents(container, config) {
|
|
110
|
+
const containerNbr = (container.container_nbr ?? "").trim();
|
|
111
|
+
if (!containerNbr) return [];
|
|
112
|
+
const ufvGkey = containerNbr;
|
|
113
|
+
const departed = parseIncosDateTime(container.out_yard_date);
|
|
114
|
+
if (departed) {
|
|
115
|
+
return [
|
|
116
|
+
baseEvent(container, config, ufvGkey, DEPARTED, departed, {
|
|
117
|
+
yardType: container.out_yard_type,
|
|
118
|
+
vesselName: container.vessel_name_out,
|
|
119
|
+
voyage: container.vessel_visit_outvoy
|
|
120
|
+
})
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
const grounded = parseIncosDateTime(container.in_yard_date);
|
|
124
|
+
if (grounded) {
|
|
125
|
+
const arrivalSpec = modeFromYardType(container.in_yard_type) === "VESSEL" ? DISCHARGED : GATE_IN;
|
|
126
|
+
return [
|
|
127
|
+
baseEvent(container, config, ufvGkey, arrivalSpec, grounded, {
|
|
128
|
+
yardType: container.in_yard_type,
|
|
129
|
+
vesselName: container.vessel_name_in,
|
|
130
|
+
voyage: container.vessel_visit_invoy
|
|
131
|
+
})
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
export {
|
|
137
|
+
combinedVisitRef,
|
|
138
|
+
directionFromCategory,
|
|
139
|
+
mapContainerToEvents,
|
|
140
|
+
modeFromYardType,
|
|
141
|
+
normalizeIncosVesselVisit,
|
|
142
|
+
parseIncosDateTime,
|
|
143
|
+
parseIncosVesselDateTime,
|
|
144
|
+
splitVisitRef
|
|
145
|
+
};
|
|
146
|
+
//# sourceMappingURL=incos-semantics.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/terminal_tracking/lib/adapters/bct/incos-semantics.ts"],
|
|
4
|
+
"sourcesContent": ["import type { IncosContainer, IncosVesselVisit } from './types'\nimport { DEFAULT_TERMINAL_TIMEZONE, zonedNaiveToUtc } from '../zoned-time'\nimport type {\n TerminalFetchedEvent,\n ResolvedTerminalConfig,\n NormalizedVesselVisit,\n} from '../../terminal-adapter'\nimport type {\n TerminalEventType,\n TerminalEventClassifierCode,\n TerminalModeOfTransport,\n} from '../../../data/entities'\n\n/**\n * INCOS returns a container *state snapshot*, not a timestamped milestone stream\n * (much like GCT). We emit only the container's CURRENT milestone \u2014 its arrival\n * (`in_yard_date`) while it is on the terminal, or its departure\n * (`out_yard_date`) once it has left \u2014 mirroring N4's `/unit` current-state row,\n * rather than synthesizing extra states we cannot timestamp. INCOS has no\n * separate loaded-onto-transport timestamp (N4's \"Loaded\" column), so no `LOAD`\n * event.\n *\n * The arrival is refined by `in_yard_type` to match N4's vocabulary: a vessel\n * arrival is a discharge (`DISC` \u2192 `equipment.discharged`), a truck/rail arrival\n * is a gate-in (`GTIN` \u2192 `equipment.gate_in`). The departure is `DEPA`\n * (`transport.departed`). All three `eventCode`s are the ones the service maps\n * via n4-semantics `semanticEventIdFor`, so no adapter-specific downstream\n * wiring is needed.\n */\n\ntype MilestoneSpec = {\n eventType: TerminalEventType\n eventCode: string\n classifier: TerminalEventClassifierCode\n}\n\nconst GATE_IN: MilestoneSpec = { eventType: 'EQUIPMENT', eventCode: 'GTIN', classifier: 'ACT' }\nconst DISCHARGED: MilestoneSpec = { eventType: 'EQUIPMENT', eventCode: 'DISC', classifier: 'ACT' }\nconst DEPARTED: MilestoneSpec = { eventType: 'TRANSPORT', eventCode: 'DEPA', classifier: 'ACT' }\n\n/** The container's direction of travel. */\nexport type IncosCargoDirection = 'import' | 'export'\n\n/**\n * Map INCOS `category` to a direction. Unlike GCT (which infers from a status\n * prefix), INCOS states it explicitly: `E` = export, `I`/`X` = import. Blank or\n * unknown yields null.\n */\nexport function directionFromCategory(category: string | null | undefined): IncosCargoDirection | null {\n const c = (category ?? '').trim().toUpperCase()\n if (!c) return null\n if (c === 'E') return 'export'\n if (c === 'I' || c === 'X') return 'import'\n return null\n}\n\n/**\n * Map an INCOS yard-move type (`in_yard_type` / `out_yard_type`) to a mode of\n * transport: `T` truck, `V` vessel, `R` rail. Blank/unknown yields null.\n */\nexport function modeFromYardType(type: string | null | undefined): TerminalModeOfTransport | null {\n const t = (type ?? '').trim().toUpperCase()\n if (t === 'T') return 'TRUCK'\n if (t === 'V') return 'VESSEL'\n if (t === 'R') return 'RAIL'\n return null\n}\n\n/**\n * Parse an INCOS datetime \u2014 `DD-MM-YYYY HH:MI` (optionally with seconds), e.g.\n * `03-05-2020 03:28`. INCOS returns terminal-local time without an offset, so we\n * interpret it in the terminal's timezone (Europe/Warsaw by default) and return\n * the corresponding UTC instant. Returns null when empty or the shape does not\n * match (a strict match avoids `Date.parse` misreading the day-first order as\n * month-first).\n */\nexport function parseIncosDateTime(\n value: string | null | undefined,\n timeZone: string = DEFAULT_TERMINAL_TIMEZONE,\n): Date | null {\n if (!value) return null\n const m = /^(\\d{2})-(\\d{2})-(\\d{4})[ T](\\d{2}):(\\d{2})(?::(\\d{2}))?$/.exec(value.trim())\n if (!m) return null\n const [, dd, mm, yyyy, hh, mi, ss] = m\n return zonedNaiveToUtc(+yyyy, +mm, +dd, +hh, +mi, ss ? +ss : 0, timeZone)\n}\n\n/**\n * Parse an INCOS **vessel-visit** datetime \u2014 `YYYY-MM-DD HH:MM[:SS]`, e.g.\n * `2026-08-13 18:00:00`. This is a DIFFERENT format from the container endpoint's\n * day-first `DD-MM-YYYY HH:MI` (confirmed against the live API). Terminal-local,\n * no offset \u2192 interpreted in the terminal's timezone, consistent with\n * `parseIncosDateTime`.\n */\nexport function parseIncosVesselDateTime(\n value: string | null | undefined,\n timeZone: string = DEFAULT_TERMINAL_TIMEZONE,\n): Date | null {\n if (!value) return null\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})[ T](\\d{2}):(\\d{2})(?::(\\d{2}))?$/.exec(value.trim())\n if (!m) return null\n const [, yyyy, mm, dd, hh, mi, ss] = m\n return zonedNaiveToUtc(+yyyy, +mm, +dd, +hh, +mi, ss ? +ss : 0, timeZone)\n}\n\n/**\n * Encode an INCOS vessel visit key. INCOS looks a visit up by (vesselCode,\n * voyage), so both are packed into one visit ref as `CODE/VOY` (vessel codes and\n * voyages contain no `/`). Returns null unless both parts are present.\n */\nexport function combinedVisitRef(\n code: string | null | undefined,\n voyage: string | null | undefined,\n): string | null {\n const c = (code ?? '').trim()\n const v = (voyage ?? '').trim()\n return c && v ? `${c}/${v}` : null\n}\n\n/** Split a `CODE/VOY` visit ref back into its parts (on the first `/`). */\nexport function splitVisitRef(visitRef: string): { vesselCode: string; voyage: string } | null {\n const i = visitRef.indexOf('/')\n if (i <= 0 || i >= visitRef.length - 1) return null\n return { vesselCode: visitRef.slice(0, i), voyage: visitRef.slice(i + 1) }\n}\n\n/**\n * Normalize an INCOS vessel-visit payload into the shared `NormalizedVesselVisit`.\n * INCOS has no `phase`, `beginReceive`, or `dryCutoff`, so those are null.\n */\nexport function normalizeIncosVesselVisit(\n data: IncosVesselVisit,\n visitRef: string,\n): NormalizedVesselVisit {\n return {\n visitRef,\n vesselName: data.vessel_name ?? null,\n ibVoyage: data.in_voy ?? null,\n obVoyage: data.out_voy ?? null,\n line: data.line_code ?? null,\n phase: null,\n eta: parseIncosVesselDateTime(data.eta),\n etd: parseIncosVesselDateTime(data.etd),\n ata: parseIncosVesselDateTime(data.ata),\n atd: parseIncosVesselDateTime(data.atd),\n beginReceive: null,\n dryCutoff: null,\n rawData: { ...data },\n }\n}\n\n/** Split a `;`-separated INCOS list, dropping blanks. */\nfunction splitList(value: string | null | undefined): string[] {\n if (!value) return []\n return value\n .split(';')\n .map((s) => s.trim())\n .filter((s) => s !== '')\n}\n\nfunction buildSourceEventId(terminalCode: string, ufvGkey: string, eventCode: string): string {\n return `${terminalCode}:${ufvGkey}:${eventCode}`\n}\n\n/** Per-milestone view of the snapshot: which yard-type + vessel fields apply. */\ntype MilestoneContext = {\n yardType: string | null | undefined\n vesselName: string | null | undefined\n voyage: string | null | undefined\n}\n\nfunction baseEvent(\n container: IncosContainer,\n config: ResolvedTerminalConfig,\n ufvGkey: string,\n spec: MilestoneSpec,\n eventDateTime: Date,\n ctx: MilestoneContext,\n): TerminalFetchedEvent {\n const seals = [container.seal1, container.seal2, container.seal3, container.seal4]\n .filter((n): n is string => typeof n === 'string' && n.trim() !== '')\n .map((number) => ({ number, source: config.terminalCode }))\n\n const direction = directionFromCategory(container.category)\n\n // Impediments (\"stopki\"). A customs HOLD is mapped to the direction-specific\n // catalogue code (`CUSTOMS IMPORT/EXPORT HOLD`) so `lib/holds.ts` classifies it\n // as a critical customs hold instead of the \"unknown\" fallback. Per the INCOS\n // docs `customs_status` is binary \u2014 empty, or `HOLD` when a customs hold is\n // placed (\"za\u0142o\u017Cony\") \u2014 so this is the full fidelity INCOS offers: it is a real\n // placed hold (not N4's routine auto-clearing \"permission\"), hence `critical`.\n // Direction unknown \u2192 the import code (the dominant BCT flow). Raw `constraint`\n // codes pass through and stay unclassified until INCOS documents them.\n const customsHold =\n String(container.customs_status ?? '').trim().toUpperCase() === 'HOLD'\n ? [direction === 'export' ? 'CUSTOMS EXPORT HOLD' : 'CUSTOMS IMPORT HOLD']\n : []\n const impediments = [...customsHold, ...splitList(container.constraint)]\n\n const vgm = typeof container.vgm_weight === 'number' ? container.vgm_weight : null\n\n return {\n source: 'terminal',\n sourceEventId: buildSourceEventId(config.terminalCode, ufvGkey, spec.eventCode),\n eventType: spec.eventType,\n eventCode: spec.eventCode,\n eventClassifierCode: spec.classifier,\n eventDateTime,\n // `ufvGkey` is the trimmed container number \u2014 reuse it so `containerNumber`\n // and the dedup key can never disagree (and to avoid a raw-field cast).\n containerNumber: ufvGkey,\n ufvGkey,\n // No S-code state on INCOS; the location is the closest transit signal.\n transitState: container.actual_location ?? null,\n visitState: null,\n facilityCode: config.facilityCode ?? null,\n facilityCodeListProvider: config.facilityCodeListProvider ?? null,\n unlocode: config.unlocode ?? null,\n // INCOS keys a vessel visit by (vesselCode, voyage), so encode both into the\n // visit ref (`CODE/VOY`); the adapter's fetchVesselVisit splits it back apart.\n visitRefIn: combinedVisitRef(container.vessel_code_in, container.vessel_visit_invoy),\n visitRefOut: combinedVisitRef(container.vessel_code_out, container.vessel_visit_outvoy),\n // Vessel name/voyage here are the container-snapshot values; when the config\n // enables the vessel endpoint the service overwrites them (and adds ETA/ATA)\n // from the resolved vessel visit.\n vesselName: ctx.vesselName ?? null,\n voyageNumber: ctx.voyage ?? null,\n modeOfTransport: modeFromYardType(ctx.yardType),\n seals: seals.length ? seals : null,\n vgmWeightKg: vgm,\n impediments: impediments.length ? impediments : null,\n loadedAt: null,\n // Preserve the full snapshot. `Category` (import/export) is the field\n // `parseCargoCategory` reads to pick the vessel-visit side for enrichment.\n rawData: { ...container, Category: direction ?? null },\n }\n}\n\n/**\n * Turn one INCOS container snapshot into zero, one, or two normalized events:\n * a gate-in when `in_yard_date` is set, and a departed when `out_yard_date` is\n * set. A snapshot with neither timestamp yields no events (nothing has provably\n * happened yet). The dedup key is the container number \u2014 the container lookup\n * carries no per-visit gkey, so (unlike GCT's `VisitNo`) a container that\n * returns for a later visit reuses the same key.\n */\nexport function mapContainerToEvents(\n container: IncosContainer,\n config: ResolvedTerminalConfig,\n): TerminalFetchedEvent[] {\n const containerNbr = (container.container_nbr ?? '').trim()\n if (!containerNbr) return []\n const ufvGkey = containerNbr\n\n // Emit only the container's CURRENT milestone, mirroring N4's `/unit` (which\n // returns a single row for the current transit state):\n // - departed (out_yard_date set) \u2192 just DEPA;\n // - still on terminal (in_yard_date set, no out) \u2192 its arrival (DISC/GTIN).\n // Emitting BOTH the historical arrival and the departure on every poll would\n // keep the job from ever completing \u2014 the service's `isFullyDeparted` requires\n // *every* event in a poll to be DEPA \u2014 so a departed container would be polled\n // forever, wasting requests against BCT's 10k/day cap. The arrival was already\n // persisted (deduped by sourceEventId) while the box sat in the yard, so it\n // stays on the timeline; only its redundant re-emission is dropped here.\n const departed = parseIncosDateTime(container.out_yard_date)\n if (departed) {\n return [\n baseEvent(container, config, ufvGkey, DEPARTED, departed, {\n yardType: container.out_yard_type,\n vesselName: container.vessel_name_out,\n voyage: container.vessel_visit_outvoy,\n }),\n ]\n }\n\n const grounded = parseIncosDateTime(container.in_yard_date)\n if (grounded) {\n // A vessel arrival is a discharge; a truck/rail (or unspecified) arrival is a\n // gate-in \u2014 matching how N4 splits DISC vs GTIN.\n const arrivalSpec = modeFromYardType(container.in_yard_type) === 'VESSEL' ? DISCHARGED : GATE_IN\n return [\n baseEvent(container, config, ufvGkey, arrivalSpec, grounded, {\n yardType: container.in_yard_type,\n vesselName: container.vessel_name_in,\n voyage: container.vessel_visit_invoy,\n }),\n ]\n }\n\n return []\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { acquireGctToken, invalidateGctToken, GctAuthError } from "./auth/token.js";
|
|
2
|
+
import { mapContainerToEvents } from "./gct-semantics.js";
|
|
3
|
+
import { n4Request } from "../n4/http.js";
|
|
4
|
+
import { terminalLogger } from "../../logger.js";
|
|
5
|
+
function joinUrl(base, path) {
|
|
6
|
+
return `${base.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
7
|
+
}
|
|
8
|
+
class GctTerminalAdapter {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.adapterType = "gct";
|
|
11
|
+
this.defaultEndpoints = {
|
|
12
|
+
// GCT's container-details endpoint occupies the generic `unit` slot.
|
|
13
|
+
unit: "/gctapi/GetContainerDetails"
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
detailsUrl(config) {
|
|
17
|
+
const path = config.endpoints?.unit?.trim() || this.defaultEndpoints.unit;
|
|
18
|
+
return joinUrl(config.baseUrl, path);
|
|
19
|
+
}
|
|
20
|
+
async getContainerDetails(containerNumber, config, token) {
|
|
21
|
+
const { status, ok, text } = await n4Request(
|
|
22
|
+
this.detailsUrl(config),
|
|
23
|
+
{
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: token,
|
|
27
|
+
Accept: "application/json",
|
|
28
|
+
"Content-Type": "application/json"
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({ CntrID: containerNumber, pageno: 1, pagesize: 100 })
|
|
31
|
+
},
|
|
32
|
+
{ proxyUrl: config.proxyUrl, label: `${config.terminalCode} GetContainerDetails` }
|
|
33
|
+
);
|
|
34
|
+
if (status === 401) {
|
|
35
|
+
invalidateGctToken(config);
|
|
36
|
+
throw new GctAuthError(`GetContainerDetails unauthorized (401) for ${config.terminalCode}`);
|
|
37
|
+
}
|
|
38
|
+
if (!ok) {
|
|
39
|
+
throw new Error(`GetContainerDetails failed (${status}) for ${config.terminalCode}`);
|
|
40
|
+
}
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(text);
|
|
44
|
+
} catch {
|
|
45
|
+
throw new Error(`GetContainerDetails response was not valid JSON for ${config.terminalCode}`);
|
|
46
|
+
}
|
|
47
|
+
return Array.isArray(parsed.Containers) ? parsed.Containers : [];
|
|
48
|
+
}
|
|
49
|
+
async fetchEvents(input) {
|
|
50
|
+
const { containerNumber, config } = input;
|
|
51
|
+
const token = await acquireGctToken(config);
|
|
52
|
+
const containers = await this.getContainerDetails(containerNumber, config, token);
|
|
53
|
+
const events = [];
|
|
54
|
+
for (const container of containers) {
|
|
55
|
+
events.push(...mapContainerToEvents(container, config));
|
|
56
|
+
}
|
|
57
|
+
terminalLogger.debug("gct fetchEvents", {
|
|
58
|
+
terminalCode: config.terminalCode,
|
|
59
|
+
container: containerNumber,
|
|
60
|
+
rows: containers.length,
|
|
61
|
+
events: events.length
|
|
62
|
+
});
|
|
63
|
+
return { events, containerNumber };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* GCT has no batch-by-container-number call, so fetch each container in turn.
|
|
67
|
+
* Kept as a distinct method (rather than falling through to per-container in
|
|
68
|
+
* the service) so the scheduler's batch path stays a single call site; the
|
|
69
|
+
* per-terminal rate limiter still throttles the underlying requests. A single
|
|
70
|
+
* container's failure is isolated so one bad number can't sink the whole poll.
|
|
71
|
+
*/
|
|
72
|
+
async fetchEventsBatch(input) {
|
|
73
|
+
const { containerNumbers, config } = input;
|
|
74
|
+
if (containerNumbers.length === 0) return { events: [] };
|
|
75
|
+
await acquireGctToken(config);
|
|
76
|
+
const events = [];
|
|
77
|
+
for (const containerNumber of containerNumbers) {
|
|
78
|
+
try {
|
|
79
|
+
const result = await this.fetchEvents({ containerNumber, config });
|
|
80
|
+
events.push(...result.events);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err instanceof GctAuthError) throw err;
|
|
83
|
+
terminalLogger.warn("gct fetchEventsBatch: container failed", {
|
|
84
|
+
terminalCode: config.terminalCode,
|
|
85
|
+
container: containerNumber,
|
|
86
|
+
message: err instanceof Error ? err.message : String(err)
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { events };
|
|
91
|
+
}
|
|
92
|
+
async testConnection(config) {
|
|
93
|
+
const started = Date.now();
|
|
94
|
+
try {
|
|
95
|
+
await acquireGctToken(config);
|
|
96
|
+
return { success: true, message: "Token acquired", latencyMs: Date.now() - started };
|
|
97
|
+
} catch (err) {
|
|
98
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
99
|
+
return { success: false, message, latencyMs: Date.now() - started };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
GctTerminalAdapter
|
|
105
|
+
};
|
|
106
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/terminal_tracking/lib/adapters/gct/adapter.ts"],
|
|
4
|
+
"sourcesContent": ["import type {\n TerminalAdapter,\n TerminalFetchResult,\n TerminalFetchedEvent,\n TerminalAdapterTestResult,\n ResolvedTerminalConfig,\n} from '../../terminal-adapter'\nimport type { GctContainer, GctContainerDetailsResponse } from './types'\nimport { acquireGctToken, invalidateGctToken, GctAuthError } from './auth/token'\nimport { mapContainerToEvents } from './gct-semantics'\nimport { n4Request } from '../n4/http'\nimport { terminalLogger } from '../../logger'\n\nfunction joinUrl(base: string, path: string): string {\n return `${base.replace(/\\/$/, '')}/${path.replace(/^\\//, '')}`\n}\n\n/**\n * Config-driven adapter for the GCT (Gdynia Container Terminal) API. One\n * instance serves every GCT terminal; per-terminal differences live in the\n * TerminalConfig.\n *\n * Differences from the N4 adapter that shape this code:\n * - Auth is a path-minted token replayed in the `Authorization` header\n * (see ./auth/token.ts), not an OAuth2 Bearer grant.\n * - There is no fetch-many-by-container-number endpoint, so `fetchEventsBatch`\n * simply loops `fetchEvents` (bounded upstream by the per-terminal limiter).\n * - There is no vessel endpoint, so `fetchVesselVisit` is intentionally omitted\n * \u2014 the service feature-detects it and skips vessel resolution for GCT.\n */\nexport class GctTerminalAdapter implements TerminalAdapter {\n readonly adapterType = 'gct'\n\n readonly defaultEndpoints = {\n // GCT's container-details endpoint occupies the generic `unit` slot.\n unit: '/gctapi/GetContainerDetails',\n }\n\n private detailsUrl(config: ResolvedTerminalConfig): string {\n const path = config.endpoints?.unit?.trim() || this.defaultEndpoints.unit\n return joinUrl(config.baseUrl, path)\n }\n\n private async getContainerDetails(\n containerNumber: string,\n config: ResolvedTerminalConfig,\n token: string,\n ): Promise<GctContainer[]> {\n const { status, ok, text } = await n4Request(\n this.detailsUrl(config),\n {\n method: 'POST',\n headers: {\n Authorization: token,\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ CntrID: containerNumber, pageno: 1, pagesize: 100 }),\n },\n { proxyUrl: config.proxyUrl, label: `${config.terminalCode} GetContainerDetails` },\n )\n\n if (status === 401) {\n invalidateGctToken(config)\n throw new GctAuthError(`GetContainerDetails unauthorized (401) for ${config.terminalCode}`)\n }\n if (!ok) {\n throw new Error(`GetContainerDetails failed (${status}) for ${config.terminalCode}`)\n }\n\n let parsed: GctContainerDetailsResponse\n try {\n parsed = JSON.parse(text) as GctContainerDetailsResponse\n } catch {\n throw new Error(`GetContainerDetails response was not valid JSON for ${config.terminalCode}`)\n }\n return Array.isArray(parsed.Containers) ? parsed.Containers : []\n }\n\n async fetchEvents(input: {\n containerNumber: string\n config: ResolvedTerminalConfig\n }): Promise<TerminalFetchResult> {\n const { containerNumber, config } = input\n const token = await acquireGctToken(config)\n const containers = await this.getContainerDetails(containerNumber, config, token)\n\n const events: TerminalFetchedEvent[] = []\n for (const container of containers) {\n events.push(...mapContainerToEvents(container, config))\n }\n\n terminalLogger.debug('gct fetchEvents', {\n terminalCode: config.terminalCode,\n container: containerNumber,\n rows: containers.length,\n events: events.length,\n })\n\n return { events, containerNumber }\n }\n\n /**\n * GCT has no batch-by-container-number call, so fetch each container in turn.\n * Kept as a distinct method (rather than falling through to per-container in\n * the service) so the scheduler's batch path stays a single call site; the\n * per-terminal rate limiter still throttles the underlying requests. A single\n * container's failure is isolated so one bad number can't sink the whole poll.\n */\n async fetchEventsBatch(input: {\n containerNumbers: string[]\n config: ResolvedTerminalConfig\n }): Promise<{ events: TerminalFetchedEvent[] }> {\n const { containerNumbers, config } = input\n if (containerNumbers.length === 0) return { events: [] }\n\n // Acquire once up front so a genuine auth failure fails the whole batch\n // (marking the job failed) instead of being swallowed 100\u00D7 as per-container\n // data errors below.\n await acquireGctToken(config)\n\n const events: TerminalFetchedEvent[] = []\n for (const containerNumber of containerNumbers) {\n try {\n const result = await this.fetchEvents({ containerNumber, config })\n events.push(...result.events)\n } catch (err) {\n // An auth error is fatal to the batch (token revoked/expired mid-run);\n // let it propagate. Data/transport errors are isolated so one bad\n // container can't sink the poll for the rest.\n if (err instanceof GctAuthError) throw err\n terminalLogger.warn('gct fetchEventsBatch: container failed', {\n terminalCode: config.terminalCode,\n container: containerNumber,\n message: err instanceof Error ? err.message : String(err),\n })\n }\n }\n return { events }\n }\n\n async testConnection(config: ResolvedTerminalConfig): Promise<TerminalAdapterTestResult> {\n const started = Date.now()\n try {\n await acquireGctToken(config)\n return { success: true, message: 'Token acquired', latencyMs: Date.now() - started }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n return { success: false, message, latencyMs: Date.now() - started }\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { n4Request } from "../../n4/http.js";
|
|
2
|
+
import { generateTotp } from "../../../totp.js";
|
|
3
|
+
const tokenCache = /* @__PURE__ */ new Map();
|
|
4
|
+
class GctAuthError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "GctAuthError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function credentials(config) {
|
|
11
|
+
const auth = config.authConfig ?? {};
|
|
12
|
+
const companyCode = String(auth.companyCode ?? "");
|
|
13
|
+
const loginName = String(auth.loginName ?? "");
|
|
14
|
+
const totpSecret = String(auth.totpSecret ?? "");
|
|
15
|
+
const legacyPassword = String(auth.loginPassword ?? "");
|
|
16
|
+
if (!companyCode || !loginName || !totpSecret && !legacyPassword) {
|
|
17
|
+
throw new GctAuthError(
|
|
18
|
+
`GCT config ${config.terminalCode} is missing companyCode/loginName/totpSecret in auth_config`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return { companyCode, loginName, totpSecret, legacyPassword };
|
|
22
|
+
}
|
|
23
|
+
function loginCode(creds) {
|
|
24
|
+
return creds.totpSecret ? generateTotp(creds.totpSecret) : creds.legacyPassword;
|
|
25
|
+
}
|
|
26
|
+
function cacheKey(config, creds) {
|
|
27
|
+
return `${config.terminalCode}:${creds.companyCode}:${creds.loginName}`;
|
|
28
|
+
}
|
|
29
|
+
function joinUrl(base, path) {
|
|
30
|
+
return `${base.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
31
|
+
}
|
|
32
|
+
const DEFAULT_AUTH_PATH = "/gctapi/Auth";
|
|
33
|
+
const DEFAULT_REFRESH_PATH = "/gctapi/ping";
|
|
34
|
+
function authUrl(config, creds, code) {
|
|
35
|
+
const authPath = config.endpoints?.auth?.trim() || DEFAULT_AUTH_PATH;
|
|
36
|
+
const segments = [creds.companyCode, creds.loginName, code].map((s) => encodeURIComponent(s)).join("/");
|
|
37
|
+
return joinUrl(config.baseUrl, `${authPath}/${segments}`);
|
|
38
|
+
}
|
|
39
|
+
function refreshUrl(config) {
|
|
40
|
+
const refreshPath = config.endpoints?.refresh?.trim() || DEFAULT_REFRESH_PATH;
|
|
41
|
+
return joinUrl(config.baseUrl, refreshPath);
|
|
42
|
+
}
|
|
43
|
+
function parseExpires(value) {
|
|
44
|
+
if (!value) return null;
|
|
45
|
+
const ms = Date.parse(value);
|
|
46
|
+
return Number.isNaN(ms) ? null : ms;
|
|
47
|
+
}
|
|
48
|
+
function tokenTtl(parsed, now) {
|
|
49
|
+
return parseExpires(parsed.expires) ?? now + 15 * 60 * 1e3;
|
|
50
|
+
}
|
|
51
|
+
async function mintGctToken(config, creds, key, now) {
|
|
52
|
+
const { status, ok, text } = await n4Request(
|
|
53
|
+
authUrl(config, creds, loginCode(creds)),
|
|
54
|
+
{ method: "GET", headers: { Accept: "application/json" } },
|
|
55
|
+
{ proxyUrl: config.proxyUrl, label: `${config.terminalCode} auth` }
|
|
56
|
+
);
|
|
57
|
+
if (!ok) {
|
|
58
|
+
throw new GctAuthError(`GCT token request failed (${status}) for ${config.terminalCode}`);
|
|
59
|
+
}
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(text);
|
|
63
|
+
} catch {
|
|
64
|
+
throw new GctAuthError(`GCT token response was not JSON for ${config.terminalCode}`);
|
|
65
|
+
}
|
|
66
|
+
if (!parsed.token) {
|
|
67
|
+
throw new GctAuthError(`No token in GCT auth response for ${config.terminalCode}`);
|
|
68
|
+
}
|
|
69
|
+
const entry = { token: parsed.token, expiresAt: tokenTtl(parsed, now) };
|
|
70
|
+
tokenCache.set(key, entry);
|
|
71
|
+
return entry.token;
|
|
72
|
+
}
|
|
73
|
+
async function refreshGctToken(config, token, now) {
|
|
74
|
+
let res;
|
|
75
|
+
try {
|
|
76
|
+
res = await n4Request(
|
|
77
|
+
refreshUrl(config),
|
|
78
|
+
{ method: "GET", headers: { Accept: "application/json", Authorization: token } },
|
|
79
|
+
{ proxyUrl: config.proxyUrl, label: `${config.terminalCode} refresh` }
|
|
80
|
+
);
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
if (!res.ok) return null;
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = JSON.parse(res.text);
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (!parsed.token) return null;
|
|
92
|
+
return { token: parsed.token, expiresAt: tokenTtl(parsed, now) };
|
|
93
|
+
}
|
|
94
|
+
async function acquireGctToken(config) {
|
|
95
|
+
const creds = credentials(config);
|
|
96
|
+
const key = cacheKey(config, creds);
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
const cached = tokenCache.get(key);
|
|
99
|
+
if (cached && cached.expiresAt > now + 3e4) return cached.token;
|
|
100
|
+
if (cached) {
|
|
101
|
+
const refreshed = await refreshGctToken(config, cached.token, now);
|
|
102
|
+
if (refreshed) {
|
|
103
|
+
tokenCache.set(key, refreshed);
|
|
104
|
+
return refreshed.token;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return mintGctToken(config, creds, key, now);
|
|
108
|
+
}
|
|
109
|
+
function invalidateGctToken(config) {
|
|
110
|
+
try {
|
|
111
|
+
tokenCache.delete(cacheKey(config, credentials(config)));
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
GctAuthError,
|
|
117
|
+
acquireGctToken,
|
|
118
|
+
invalidateGctToken
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=token.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../src/modules/terminal_tracking/lib/adapters/gct/auth/token.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ResolvedTerminalConfig } from '../../../terminal-adapter'\nimport type { GctToken } from '../types'\nimport { n4Request } from '../../n4/http'\nimport { generateTotp } from '../../../totp'\n\n/**\n * GCT token acquisition + process-level cache.\n *\n * GCT mints a token from a plain `GET /gctapi/Auth/{company}/{login}/{password}`\n * (credentials in the path \u2014 see the security note below), returning an ISO\n * `expires` instant. The token is then replayed in the `Authorization` header on\n * every data call. This mirrors `../../n4/auth/ropc.ts` in shape (cache keyed by\n * principal, 30s safety margin, invalidate-on-401) but not in wire protocol.\n *\n * LIFECYCLE: a cached token is reused until 30s before `expires`. Once it lapses\n * we first try to REFRESH it via the keep-alive endpoint (`GET /gctapi/ping`,\n * current token in the header) \u2014 cheap, no 2FA code \u2014 and only RE-AUTHENTICATE\n * (mint a new token with a fresh authenticator code) when the refresh fails.\n *\n * AUTH: GCT's login expects an authenticator one-time code where a password would\n * normally go. We store the base32 authenticator *secret* (the seed behind the\n * enrollment QR) in `auth_config` and derive the current 6-digit TOTP code at\n * mint time \u2014 never a static password. A legacy static `loginPassword` is still\n * honoured as a fallback so pre-existing configs don't hard-break.\n *\n * SECURITY: the credential (the derived code) is in the request URL, and the\n * secret must never appear anywhere. We NEVER log the URL \u2014 the shared\n * `n4Request` helper only logs the caller-supplied `label`, so we always pass an\n * opaque label and never the URL, and errors below never echo it. Only the\n * short-lived derived code ever reaches the URL; the seed does not.\n */\n\ntype CachedToken = { token: string; expiresAt: number }\n\nconst tokenCache = new Map<string, CachedToken>()\n\nexport class GctAuthError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'GctAuthError'\n }\n}\n\ntype GctCredentials = { companyCode: string; loginName: string; totpSecret: string; legacyPassword: string }\n\nfunction credentials(config: ResolvedTerminalConfig): GctCredentials {\n const auth = (config.authConfig ?? {}) as Record<string, unknown>\n const companyCode = String(auth.companyCode ?? '')\n const loginName = String(auth.loginName ?? '')\n const totpSecret = String(auth.totpSecret ?? '')\n // Legacy fallback: pre-TOTP configs stored a static `loginPassword`.\n const legacyPassword = String(auth.loginPassword ?? '')\n if (!companyCode || !loginName || (!totpSecret && !legacyPassword)) {\n throw new GctAuthError(\n `GCT config ${config.terminalCode} is missing companyCode/loginName/totpSecret in auth_config`,\n )\n }\n return { companyCode, loginName, totpSecret, legacyPassword }\n}\n\n/**\n * The value GCT expects in the password path segment: the current TOTP code\n * derived from the authenticator secret, or a legacy static password when no\n * secret is configured.\n */\nfunction loginCode(creds: GctCredentials): string {\n return creds.totpSecret ? generateTotp(creds.totpSecret) : creds.legacyPassword\n}\n\nfunction cacheKey(config: ResolvedTerminalConfig, creds: GctCredentials): string {\n // Keyed by principal, NOT org/tenant: identical credentials mint the same\n // token, so sharing one cache entry is correct. Persisted rows stay\n // org-scoped independently of this cache.\n return `${config.terminalCode}:${creds.companyCode}:${creds.loginName}`\n}\n\nfunction joinUrl(base: string, path: string): string {\n return `${base.replace(/\\/$/, '')}/${path.replace(/^\\//, '')}`\n}\n\n/** Default GCT auth path; a config may override via `endpoints.auth`. */\nconst DEFAULT_AUTH_PATH = '/gctapi/Auth'\n\n/** Default GCT token-refresh (keep-alive) path; override via `endpoints.refresh`. */\nconst DEFAULT_REFRESH_PATH = '/gctapi/ping'\n\nfunction authUrl(config: ResolvedTerminalConfig, creds: GctCredentials, code: string): string {\n const authPath =\n (config.endpoints as { auth?: string } | undefined)?.auth?.trim() || DEFAULT_AUTH_PATH\n const segments = [creds.companyCode, creds.loginName, code]\n .map((s) => encodeURIComponent(s))\n .join('/')\n return joinUrl(config.baseUrl, `${authPath}/${segments}`)\n}\n\nfunction refreshUrl(config: ResolvedTerminalConfig): string {\n const refreshPath =\n (config.endpoints as { refresh?: string } | undefined)?.refresh?.trim() || DEFAULT_REFRESH_PATH\n return joinUrl(config.baseUrl, refreshPath)\n}\n\n/** Parse GCT's ISO `expires` into epoch ms; null when unparseable. */\nfunction parseExpires(value: string | undefined): number | null {\n if (!value) return null\n const ms = Date.parse(value)\n return Number.isNaN(ms) ? null : ms\n}\n\n/** Fall back to a conservative 15-minute TTL when `expires` is missing/bad. */\nfunction tokenTtl(parsed: GctToken, now: number): number {\n return parseExpires(parsed.expires) ?? now + 15 * 60 * 1000\n}\n\n/**\n * Full (re-)authentication: derive a fresh authenticator code (it rotates every\n * 30s) and mint a new token from the path-GET Auth endpoint. Caches and returns\n * the raw token. This spends one 2FA code, so it is the fallback path \u2014 callers\n * try {@link refreshGctToken} first when they still hold a token.\n */\nasync function mintGctToken(\n config: ResolvedTerminalConfig,\n creds: GctCredentials,\n key: string,\n now: number,\n): Promise<string> {\n const { status, ok, text } = await n4Request(\n authUrl(config, creds, loginCode(creds)),\n { method: 'GET', headers: { Accept: 'application/json' } },\n { proxyUrl: config.proxyUrl, label: `${config.terminalCode} auth` },\n )\n\n if (!ok) {\n // Do not include the URL (it carries the derived auth code); status only.\n throw new GctAuthError(`GCT token request failed (${status}) for ${config.terminalCode}`)\n }\n\n let parsed: GctToken\n try {\n parsed = JSON.parse(text) as GctToken\n } catch {\n throw new GctAuthError(`GCT token response was not JSON for ${config.terminalCode}`)\n }\n if (!parsed.token) {\n throw new GctAuthError(`No token in GCT auth response for ${config.terminalCode}`)\n }\n\n const entry: CachedToken = { token: parsed.token, expiresAt: tokenTtl(parsed, now) }\n tokenCache.set(key, entry)\n return entry.token\n}\n\n/**\n * Extend the current token via GCT's keep-alive endpoint\n * (`GET /gctapi/ping`, current token replayed in the `Authorization` header).\n * Returns the refreshed cache entry on success, or `null` on ANY failure \u2014\n * non-2xx, non-JSON, no token in the body, or a network error \u2014 so the caller\n * falls back to a full re-authentication. Never throws (no 2FA code is spent).\n */\nasync function refreshGctToken(\n config: ResolvedTerminalConfig,\n token: string,\n now: number,\n): Promise<CachedToken | null> {\n let res\n try {\n res = await n4Request(\n refreshUrl(config),\n { method: 'GET', headers: { Accept: 'application/json', Authorization: token } },\n { proxyUrl: config.proxyUrl, label: `${config.terminalCode} refresh` },\n )\n } catch {\n // Transient network failure exhausted its retries \u2192 re-authenticate.\n return null\n }\n if (!res.ok) return null\n let parsed: GctToken\n try {\n parsed = JSON.parse(res.text) as GctToken\n } catch {\n return null\n }\n if (!parsed.token) return null\n return { token: parsed.token, expiresAt: tokenTtl(parsed, now) }\n}\n\n/**\n * Acquire (and cache) a GCT token for the `Authorization` header. A cached token\n * is reused until 30s before `expires`. Once it lapses, we first try to REFRESH\n * it via the keep-alive endpoint (cheap \u2014 no 2FA code); only if that fails do we\n * fully RE-AUTHENTICATE by minting a new token with a fresh authenticator code.\n */\nexport async function acquireGctToken(config: ResolvedTerminalConfig): Promise<string> {\n const creds = credentials(config)\n const key = cacheKey(config, creds)\n const now = Date.now()\n const cached = tokenCache.get(key)\n if (cached && cached.expiresAt > now + 30_000) return cached.token\n\n // (Near-)expired but a token is still in hand: try the keep-alive refresh\n // before spending a fresh 2FA code. Any refresh failure \u2192 full re-auth.\n if (cached) {\n const refreshed = await refreshGctToken(config, cached.token, now)\n if (refreshed) {\n tokenCache.set(key, refreshed)\n return refreshed.token\n }\n }\n\n return mintGctToken(config, creds, key, now)\n}\n\n/** Clear the cached token for a terminal (used after a 401). */\nexport function invalidateGctToken(config: ResolvedTerminalConfig): void {\n try {\n tokenCache.delete(cacheKey(config, credentials(config)))\n } catch {\n // Missing creds \u2192 nothing cached; ignore.\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { DEFAULT_TERMINAL_TIMEZONE, zonedNaiveToUtc } from "../zoned-time.js";
|
|
2
|
+
const GATE_IN = { eventType: "EQUIPMENT", eventCode: "GTIN", classifier: "ACT" };
|
|
3
|
+
const DEPARTED = { eventType: "TRANSPORT", eventCode: "DEPA", classifier: "ACT" };
|
|
4
|
+
function directionFromStatus(status) {
|
|
5
|
+
const s = (status ?? "").trim().toUpperCase();
|
|
6
|
+
if (!s) return null;
|
|
7
|
+
if (s.startsWith("X")) return "export";
|
|
8
|
+
if (s.startsWith("I")) return "import";
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
function parseGctDateTime(value, timeZone = DEFAULT_TERMINAL_TIMEZONE) {
|
|
12
|
+
if (!value) return null;
|
|
13
|
+
const raw = value.trim();
|
|
14
|
+
if (!raw) return null;
|
|
15
|
+
if (/[zZ]$|[+-]\d{2}:?\d{2}$/.test(raw)) {
|
|
16
|
+
const ms = Date.parse(raw.replace(" ", "T"));
|
|
17
|
+
return Number.isNaN(ms) ? null : new Date(ms);
|
|
18
|
+
}
|
|
19
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/.exec(raw);
|
|
20
|
+
if (!m) return null;
|
|
21
|
+
const [, yyyy, mm, dd, hh, mi, ss] = m;
|
|
22
|
+
return zonedNaiveToUtc(+yyyy, +mm, +dd, +hh, +mi, ss ? +ss : 0, timeZone);
|
|
23
|
+
}
|
|
24
|
+
function buildSourceEventId(terminalCode, ufvGkey, eventCode) {
|
|
25
|
+
return `${terminalCode}:${ufvGkey}:${eventCode}`;
|
|
26
|
+
}
|
|
27
|
+
function baseEvent(container, config, ufvGkey, spec, eventDateTime) {
|
|
28
|
+
const seals = (container.SealList ?? []).filter((n) => typeof n === "string" && n.trim() !== "").map((number) => ({ number, source: config.terminalCode }));
|
|
29
|
+
const holds = (container.HoldCodesList ?? []).filter(
|
|
30
|
+
(c) => typeof c === "string" && c.trim() !== ""
|
|
31
|
+
);
|
|
32
|
+
const direction = directionFromStatus(container.CntrStatus);
|
|
33
|
+
return {
|
|
34
|
+
source: "terminal",
|
|
35
|
+
sourceEventId: buildSourceEventId(config.terminalCode, ufvGkey, spec.eventCode),
|
|
36
|
+
eventType: spec.eventType,
|
|
37
|
+
eventCode: spec.eventCode,
|
|
38
|
+
eventClassifierCode: spec.classifier,
|
|
39
|
+
eventDateTime,
|
|
40
|
+
containerNumber: container.CntrID,
|
|
41
|
+
ufvGkey,
|
|
42
|
+
transitState: container.CntrStatus ?? null,
|
|
43
|
+
visitState: null,
|
|
44
|
+
facilityCode: config.facilityCode ?? null,
|
|
45
|
+
facilityCodeListProvider: config.facilityCodeListProvider ?? null,
|
|
46
|
+
unlocode: config.unlocode ?? null,
|
|
47
|
+
visitRefIn: null,
|
|
48
|
+
visitRefOut: null,
|
|
49
|
+
// Vessel name/voyage are metadata only — GCT has no vessel-visit endpoint, so
|
|
50
|
+
// no schedule (ETA/ATA) enrichment is possible.
|
|
51
|
+
vesselName: container.VesselName ?? null,
|
|
52
|
+
voyageNumber: container.GCTVoyage ?? container.OwnerVoyage ?? null,
|
|
53
|
+
modeOfTransport: "VESSEL",
|
|
54
|
+
seals: seals.length ? seals : null,
|
|
55
|
+
vgmWeightKg: typeof container.VGMWeight === "number" ? container.VGMWeight : null,
|
|
56
|
+
impediments: holds.length ? holds : null,
|
|
57
|
+
loadedAt: null,
|
|
58
|
+
// Preserve the full snapshot plus a normalized direction for downstream use.
|
|
59
|
+
rawData: { ...container, Category: direction ?? null }
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function mapContainerToEvents(container, config) {
|
|
63
|
+
if (!container.CntrID) return [];
|
|
64
|
+
const ufvGkey = container.VisitNo && container.VisitNo.trim() || container.CntrID;
|
|
65
|
+
const events = [];
|
|
66
|
+
const grounded = parseGctDateTime(container.GroundingDateTime);
|
|
67
|
+
if (grounded) events.push(baseEvent(container, config, ufvGkey, GATE_IN, grounded));
|
|
68
|
+
const pickedUp = parseGctDateTime(container.PickupDateTime);
|
|
69
|
+
if (pickedUp) events.push(baseEvent(container, config, ufvGkey, DEPARTED, pickedUp));
|
|
70
|
+
return events;
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
directionFromStatus,
|
|
74
|
+
mapContainerToEvents,
|
|
75
|
+
parseGctDateTime
|
|
76
|
+
};
|
|
77
|
+
//# sourceMappingURL=gct-semantics.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/terminal_tracking/lib/adapters/gct/gct-semantics.ts"],
|
|
4
|
+
"sourcesContent": ["import type { GctContainer } from './types'\nimport { DEFAULT_TERMINAL_TIMEZONE, zonedNaiveToUtc } from '../zoned-time'\nimport type { TerminalFetchedEvent, ResolvedTerminalConfig } from '../../terminal-adapter'\nimport type { TerminalEventType, TerminalEventClassifierCode } from '../../../data/entities'\n\n/**\n * GCT returns a container *state snapshot*, not a timestamped milestone stream.\n * We emit only the two milestones GCT actually timestamps \u2014 grounding (gate-in)\n * and pickup (gate-out/departed) \u2014 rather than synthesizing discharge/load from\n * a status code we cannot timestamp. The `eventCode`s (`GTIN`/`DEPA`) are the\n * same the service maps to `equipment.gate_in` / `transport.departed`, so no\n * adapter-specific wiring is needed downstream.\n */\n\ntype MilestoneSpec = {\n eventType: TerminalEventType\n eventCode: string\n classifier: TerminalEventClassifierCode\n}\n\nconst GATE_IN: MilestoneSpec = { eventType: 'EQUIPMENT', eventCode: 'GTIN', classifier: 'ACT' }\nconst DEPARTED: MilestoneSpec = { eventType: 'TRANSPORT', eventCode: 'DEPA', classifier: 'ACT' }\n\n/** The container's direction of travel, inferred from the GCT status prefix. */\nexport type GctCargoDirection = 'import' | 'export'\n\n/**\n * Map `CntrStatus` (XF/EM/XM/IF/XI) to a direction: `X*` = export, `I*` = import.\n * `EM` (empty, no purpose) and any unknown value yield null. Kept deliberately\n * lenient \u2014 the vendor docs contradict on `IF` (full vs empty) and list an\n * undocumented `XI`, but Phase 1 does not branch on the full/empty nuance.\n */\nexport function directionFromStatus(status: string | null | undefined): GctCargoDirection | null {\n const s = (status ?? '').trim().toUpperCase()\n if (!s) return null\n if (s.startsWith('X')) return 'export'\n if (s.startsWith('I')) return 'import'\n return null\n}\n\n/**\n * Parse a GCT datetime string. GCT is inconsistent \u2014 some fields are ISO-8601\n * with an explicit offset, others look like N4's \"yyyy-mm-dd hh:mm\"\n * (terminal-local, no offset). A field that carries its own timezone is parsed\n * as-is; a naive one is interpreted in the terminal's timezone (Europe/Warsaw\n * by default). Returns null when unparseable or empty.\n */\nexport function parseGctDateTime(\n value: string | null | undefined,\n timeZone: string = DEFAULT_TERMINAL_TIMEZONE,\n): Date | null {\n if (!value) return null\n const raw = value.trim()\n if (!raw) return null\n // An explicit timezone (`Z` or `\u00B1hh:mm`) is authoritative \u2014 parse directly.\n if (/[zZ]$|[+-]\\d{2}:?\\d{2}$/.test(raw)) {\n const ms = Date.parse(raw.replace(' ', 'T'))\n return Number.isNaN(ms) ? null : new Date(ms)\n }\n // Otherwise it is terminal-local-without-offset: interpret it in `timeZone`.\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})[ T](\\d{2}):(\\d{2})(?::(\\d{2}))?$/.exec(raw)\n if (!m) return null\n const [, yyyy, mm, dd, hh, mi, ss] = m\n return zonedNaiveToUtc(+yyyy, +mm, +dd, +hh, +mi, ss ? +ss : 0, timeZone)\n}\n\nfunction buildSourceEventId(terminalCode: string, ufvGkey: string, eventCode: string): string {\n return `${terminalCode}:${ufvGkey}:${eventCode}`\n}\n\nfunction baseEvent(\n container: GctContainer,\n config: ResolvedTerminalConfig,\n ufvGkey: string,\n spec: MilestoneSpec,\n eventDateTime: Date,\n): TerminalFetchedEvent {\n const seals = (container.SealList ?? [])\n .filter((n): n is string => typeof n === 'string' && n.trim() !== '')\n .map((number) => ({ number, source: config.terminalCode }))\n const holds = (container.HoldCodesList ?? []).filter(\n (c): c is string => typeof c === 'string' && c.trim() !== '',\n )\n const direction = directionFromStatus(container.CntrStatus)\n\n return {\n source: 'terminal',\n sourceEventId: buildSourceEventId(config.terminalCode, ufvGkey, spec.eventCode),\n eventType: spec.eventType,\n eventCode: spec.eventCode,\n eventClassifierCode: spec.classifier,\n eventDateTime,\n containerNumber: container.CntrID,\n ufvGkey,\n transitState: container.CntrStatus ?? null,\n visitState: null,\n facilityCode: config.facilityCode ?? null,\n facilityCodeListProvider: config.facilityCodeListProvider ?? null,\n unlocode: config.unlocode ?? null,\n visitRefIn: null,\n visitRefOut: null,\n // Vessel name/voyage are metadata only \u2014 GCT has no vessel-visit endpoint, so\n // no schedule (ETA/ATA) enrichment is possible.\n vesselName: container.VesselName ?? null,\n voyageNumber: container.GCTVoyage ?? container.OwnerVoyage ?? null,\n modeOfTransport: 'VESSEL',\n seals: seals.length ? seals : null,\n vgmWeightKg: typeof container.VGMWeight === 'number' ? container.VGMWeight : null,\n impediments: holds.length ? holds : null,\n loadedAt: null,\n // Preserve the full snapshot plus a normalized direction for downstream use.\n rawData: { ...container, Category: direction ?? null },\n }\n}\n\n/**\n * Turn one GCT container snapshot into zero, one, or two normalized events:\n * a gate-in when `GroundingDateTime` is set, and a departed when\n * `PickupDateTime` is set. A snapshot with neither timestamp yields no events\n * (nothing has provably happened yet).\n */\nexport function mapContainerToEvents(\n container: GctContainer,\n config: ResolvedTerminalConfig,\n): TerminalFetchedEvent[] {\n if (!container.CntrID) return []\n const ufvGkey = (container.VisitNo && container.VisitNo.trim()) || container.CntrID\n\n const events: TerminalFetchedEvent[] = []\n const grounded = parseGctDateTime(container.GroundingDateTime)\n if (grounded) events.push(baseEvent(container, config, ufvGkey, GATE_IN, grounded))\n const pickedUp = parseGctDateTime(container.PickupDateTime)\n if (pickedUp) events.push(baseEvent(container, config, ufvGkey, DEPARTED, pickedUp))\n\n return events\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,2BAA2B,uBAAuB;AAmB3D,MAAM,UAAyB,EAAE,WAAW,aAAa,WAAW,QAAQ,YAAY,MAAM;AAC9F,MAAM,WAA0B,EAAE,WAAW,aAAa,WAAW,QAAQ,YAAY,MAAM;AAWxF,SAAS,oBAAoB,QAA6D;AAC/F,QAAM,KAAK,UAAU,IAAI,KAAK,EAAE,YAAY;AAC5C,MAAI,CAAC,EAAG,QAAO;AACf,MAAI,EAAE,WAAW,GAAG,EAAG,QAAO;AAC9B,MAAI,EAAE,WAAW,GAAG,EAAG,QAAO;AAC9B,SAAO;AACT;AASO,SAAS,iBACd,OACA,WAAmB,2BACN;AACb,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,MAAM,MAAM,KAAK;AACvB,MAAI,CAAC,IAAK,QAAO;AAEjB,MAAI,0BAA0B,KAAK,GAAG,GAAG;AACvC,UAAM,KAAK,KAAK,MAAM,IAAI,QAAQ,KAAK,GAAG,CAAC;AAC3C,WAAO,OAAO,MAAM,EAAE,IAAI,OAAO,IAAI,KAAK,EAAE;AAAA,EAC9C;AAEA,QAAM,IAAI,4DAA4D,KAAK,GAAG;AAC9E,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,CAAC,EAAE,MAAM,IAAI,IAAI,IAAI,IAAI,EAAE,IAAI;AACrC,SAAO,gBAAgB,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,KAAK,GAAG,QAAQ;AAC1E;AAEA,SAAS,mBAAmB,cAAsB,SAAiB,WAA2B;AAC5F,SAAO,GAAG,YAAY,IAAI,OAAO,IAAI,SAAS;AAChD;AAEA,SAAS,UACP,WACA,QACA,SACA,MACA,eACsB;AACtB,QAAM,SAAS,UAAU,YAAY,CAAC,GACnC,OAAO,CAAC,MAAmB,OAAO,MAAM,YAAY,EAAE,KAAK,MAAM,EAAE,EACnE,IAAI,CAAC,YAAY,EAAE,QAAQ,QAAQ,OAAO,aAAa,EAAE;AAC5D,QAAM,SAAS,UAAU,iBAAiB,CAAC,GAAG;AAAA,IAC5C,CAAC,MAAmB,OAAO,MAAM,YAAY,EAAE,KAAK,MAAM;AAAA,EAC5D;AACA,QAAM,YAAY,oBAAoB,UAAU,UAAU;AAE1D,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,eAAe,mBAAmB,OAAO,cAAc,SAAS,KAAK,SAAS;AAAA,IAC9E,WAAW,KAAK;AAAA,IAChB,WAAW,KAAK;AAAA,IAChB,qBAAqB,KAAK;AAAA,IAC1B;AAAA,IACA,iBAAiB,UAAU;AAAA,IAC3B;AAAA,IACA,cAAc,UAAU,cAAc;AAAA,IACtC,YAAY;AAAA,IACZ,cAAc,OAAO,gBAAgB;AAAA,IACrC,0BAA0B,OAAO,4BAA4B;AAAA,IAC7D,UAAU,OAAO,YAAY;AAAA,IAC7B,YAAY;AAAA,IACZ,aAAa;AAAA;AAAA;AAAA,IAGb,YAAY,UAAU,cAAc;AAAA,IACpC,cAAc,UAAU,aAAa,UAAU,eAAe;AAAA,IAC9D,iBAAiB;AAAA,IACjB,OAAO,MAAM,SAAS,QAAQ;AAAA,IAC9B,aAAa,OAAO,UAAU,cAAc,WAAW,UAAU,YAAY;AAAA,IAC7E,aAAa,MAAM,SAAS,QAAQ;AAAA,IACpC,UAAU;AAAA;AAAA,IAEV,SAAS,EAAE,GAAG,WAAW,UAAU,aAAa,KAAK;AAAA,EACvD;AACF;AAQO,SAAS,qBACd,WACA,QACwB;AACxB,MAAI,CAAC,UAAU,OAAQ,QAAO,CAAC;AAC/B,QAAM,UAAW,UAAU,WAAW,UAAU,QAAQ,KAAK,KAAM,UAAU;AAE7E,QAAM,SAAiC,CAAC;AACxC,QAAM,WAAW,iBAAiB,UAAU,iBAAiB;AAC7D,MAAI,SAAU,QAAO,KAAK,UAAU,WAAW,QAAQ,SAAS,SAAS,QAAQ,CAAC;AAClF,QAAM,WAAW,iBAAiB,UAAU,cAAc;AAC1D,MAAI,SAAU,QAAO,KAAK,UAAU,WAAW,QAAQ,SAAS,UAAU,QAAQ,CAAC;AAEnF,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { N4TerminalAdapter } from "./n4/adapter.js";
|
|
2
|
+
import { GctTerminalAdapter } from "./gct/adapter.js";
|
|
3
|
+
import { BctTerminalAdapter } from "./bct/adapter.js";
|
|
4
|
+
function registerAllAdapters(registry) {
|
|
5
|
+
registry.register(new N4TerminalAdapter());
|
|
6
|
+
registry.register(new GctTerminalAdapter());
|
|
7
|
+
registry.register(new BctTerminalAdapter());
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
registerAllAdapters
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/terminal_tracking/lib/adapters/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type { TerminalRegistry } from '../../services/terminalRegistry'\nimport { N4TerminalAdapter } from './n4/adapter'\nimport { GctTerminalAdapter } from './gct/adapter'\nimport { BctTerminalAdapter } from './bct/adapter'\n\n/**\n * Register all built-in terminal adapters. Adapters are keyed by `adapterType`;\n * a single adapter serves every terminal of that type (N4 \u2192 Baltic Hub etc.,\n * GCT \u2192 Gdynia Container Terminal, BCT \u2192 Ba\u0142tycki Terminal Kontenerowy via\n * INCOS), which are configured as `TerminalConfig` rows, not hardcoded here.\n */\nexport function registerAllAdapters(registry: TerminalRegistry): void {\n registry.register(new N4TerminalAdapter())\n registry.register(new GctTerminalAdapter())\n registry.register(new BctTerminalAdapter())\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,yBAAyB;AAClC,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AAQ5B,SAAS,oBAAoB,UAAkC;AACpE,WAAS,SAAS,IAAI,kBAAkB,CAAC;AACzC,WAAS,SAAS,IAAI,mBAAmB,CAAC;AAC1C,WAAS,SAAS,IAAI,mBAAmB,CAAC;AAC5C;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|