@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,238 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// The adapter's fetches run through `n4Request`, which drives undici's `fetch`.
|
|
4
|
+
// Mock the undici module, keeping the real ProxyAgent/Agent so the dispatcher is
|
|
5
|
+
// built exactly as in production.
|
|
6
|
+
const { fetchMock } = vi.hoisted(() => ({ fetchMock: vi.fn() }))
|
|
7
|
+
vi.mock('undici', async (orig) => {
|
|
8
|
+
const actual = await orig<typeof import('undici')>()
|
|
9
|
+
return { ...actual, fetch: fetchMock }
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
import { GctTerminalAdapter } from '../adapter'
|
|
13
|
+
import type { ResolvedTerminalConfig } from '../../../terminal-adapter'
|
|
14
|
+
|
|
15
|
+
let counter = 0
|
|
16
|
+
function cfg(overrides: Partial<ResolvedTerminalConfig> = {}): ResolvedTerminalConfig {
|
|
17
|
+
// Unique terminalCode per config to bypass the process-level token cache.
|
|
18
|
+
return {
|
|
19
|
+
terminalCode: `gcttest-${counter++}`,
|
|
20
|
+
adapterType: 'gct',
|
|
21
|
+
displayName: 'GCT',
|
|
22
|
+
baseUrl: 'https://api.gct.example',
|
|
23
|
+
proxyUrl: null,
|
|
24
|
+
endpoints: { unit: '/gctapi/GetContainerDetails' },
|
|
25
|
+
authType: 'gct_token',
|
|
26
|
+
// Authenticator secret (base32) — GCT's login takes a TOTP code as password.
|
|
27
|
+
// `JBSWY3DPEHPK3PXP` is the canonical RFC test seed ("Hello!").
|
|
28
|
+
authConfig: { companyCode: 'ACME', loginName: 'user', totpSecret: 'JBSWY3DPEHPK3PXP' },
|
|
29
|
+
rateLimitRequests: 60,
|
|
30
|
+
rateLimitWindowSeconds: 60,
|
|
31
|
+
unlocode: 'PLGDY',
|
|
32
|
+
...overrides,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const FUTURE = '2999-01-01T00:00:00Z'
|
|
37
|
+
const PAST = '2000-01-01T00:00:00Z'
|
|
38
|
+
|
|
39
|
+
function tokenResponse(token = 'tok-123', expires: string = FUTURE) {
|
|
40
|
+
return new Response(JSON.stringify({ token, token_type: 'Bearer', expires }), { status: 200 })
|
|
41
|
+
}
|
|
42
|
+
function detailsResponse(containers: unknown[]) {
|
|
43
|
+
return new Response(JSON.stringify({ Total: containers.length, Containers: containers }), { status: 200 })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const isAuthCall = (c: any[]) => (c[1] as any)?.method === 'GET'
|
|
47
|
+
const isDetailsCall = (c: any[]) => (c[1] as any)?.method === 'POST'
|
|
48
|
+
// Mint (full auth) vs refresh (keep-alive) are both GET — tell them apart by URL.
|
|
49
|
+
const isMintCall = (c: any[]) => String(c[0]).includes('/gctapi/Auth')
|
|
50
|
+
const isRefreshCall = (c: any[]) => String(c[0]).includes('/gctapi/ping')
|
|
51
|
+
|
|
52
|
+
afterEach(() => fetchMock.mockReset())
|
|
53
|
+
|
|
54
|
+
describe('GctTerminalAdapter.testConnection', () => {
|
|
55
|
+
// TC-TRACK-301
|
|
56
|
+
it('mints a token and reports success; a generated TOTP code goes in the path, token in the Authorization header', async () => {
|
|
57
|
+
fetchMock.mockResolvedValueOnce(tokenResponse('abc'))
|
|
58
|
+
const config = cfg()
|
|
59
|
+
const res = await new GctTerminalAdapter().testConnection(config)
|
|
60
|
+
|
|
61
|
+
expect(res.success).toBe(true)
|
|
62
|
+
const authCall = fetchMock.mock.calls.find(isAuthCall)!
|
|
63
|
+
// company/login in the path; the password segment is a fresh 6-digit TOTP code.
|
|
64
|
+
expect(String(authCall[0])).toMatch(/\/gctapi\/Auth\/ACME\/user\/\d{6}$/)
|
|
65
|
+
// The raw authenticator secret must NEVER appear in the URL — only the code.
|
|
66
|
+
expect(String(authCall[0])).not.toContain('JBSWY3DPEHPK3PXP')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('honours a legacy static loginPassword when no totpSecret is configured', async () => {
|
|
70
|
+
fetchMock.mockResolvedValueOnce(tokenResponse('abc'))
|
|
71
|
+
const config = cfg({ authConfig: { companyCode: 'ACME', loginName: 'user', loginPassword: 'legacy-pass' } })
|
|
72
|
+
const res = await new GctTerminalAdapter().testConnection(config)
|
|
73
|
+
|
|
74
|
+
expect(res.success).toBe(true)
|
|
75
|
+
const authCall = fetchMock.mock.calls.find(isAuthCall)!
|
|
76
|
+
expect(String(authCall[0])).toContain('/gctapi/Auth/ACME/user/legacy-pass')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('reports failure with only the status (never the secret) on a bad token', async () => {
|
|
80
|
+
fetchMock.mockResolvedValueOnce(new Response('nope', { status: 403 }))
|
|
81
|
+
const res = await new GctTerminalAdapter().testConnection(cfg())
|
|
82
|
+
expect(res.success).toBe(false)
|
|
83
|
+
expect(res.message).toContain('403')
|
|
84
|
+
expect(res.message).not.toContain('JBSWY3DPEHPK3PXP')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('fails clearly when auth_config is missing credentials', async () => {
|
|
88
|
+
const res = await new GctTerminalAdapter().testConnection(cfg({ authConfig: {} }))
|
|
89
|
+
expect(res.success).toBe(false)
|
|
90
|
+
expect(res.message).toMatch(/companyCode|totpSecret/)
|
|
91
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('GctTerminalAdapter.fetchEvents', () => {
|
|
96
|
+
// TC-TRACK-306 (mapping + Authorization header form)
|
|
97
|
+
it('replays the raw token in the Authorization header and maps the container', async () => {
|
|
98
|
+
fetchMock.mockResolvedValueOnce(tokenResponse('tok-xyz'))
|
|
99
|
+
fetchMock.mockResolvedValueOnce(
|
|
100
|
+
detailsResponse([{ CntrID: 'GCTU1234567', VisitNo: 'V9', GroundingDateTime: '2026-08-10T09:30:00Z' }]),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const { events } = await new GctTerminalAdapter().fetchEvents({
|
|
104
|
+
containerNumber: 'GCTU1234567',
|
|
105
|
+
config: cfg(),
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
expect(events).toHaveLength(1)
|
|
109
|
+
expect(events[0].sourceEventId).toBe(events[0].sourceEventId) // stable
|
|
110
|
+
expect(events[0].eventCode).toBe('GTIN')
|
|
111
|
+
|
|
112
|
+
const detailsCall = fetchMock.mock.calls.find(isDetailsCall)!
|
|
113
|
+
expect((detailsCall[1] as any).headers.Authorization).toBe('tok-xyz')
|
|
114
|
+
expect(JSON.parse((detailsCall[1] as any).body)).toMatchObject({ CntrID: 'GCTU1234567' })
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// TC-TRACK-309
|
|
118
|
+
it('returns [] for an empty/malformed Containers payload without throwing', async () => {
|
|
119
|
+
fetchMock.mockResolvedValueOnce(tokenResponse())
|
|
120
|
+
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ Total: 0 }), { status: 200 }))
|
|
121
|
+
const { events } = await new GctTerminalAdapter().fetchEvents({ containerNumber: 'X', config: cfg() })
|
|
122
|
+
expect(events).toEqual([])
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('throws on a non-JSON details body', async () => {
|
|
126
|
+
fetchMock.mockResolvedValueOnce(tokenResponse())
|
|
127
|
+
fetchMock.mockResolvedValueOnce(new Response('<html>500</html>', { status: 200 }))
|
|
128
|
+
await expect(
|
|
129
|
+
new GctTerminalAdapter().fetchEvents({ containerNumber: 'X', config: cfg() }),
|
|
130
|
+
).rejects.toThrow(/valid JSON/)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('GctTerminalAdapter token cache + 401 handling', () => {
|
|
135
|
+
// TC-TRACK-305
|
|
136
|
+
it('reuses the cached token across calls, then re-acquires after a 401', async () => {
|
|
137
|
+
const config = cfg()
|
|
138
|
+
// 1st fetchEvents: token GET + details POST(200)
|
|
139
|
+
fetchMock.mockResolvedValueOnce(tokenResponse('t1'))
|
|
140
|
+
fetchMock.mockResolvedValueOnce(detailsResponse([]))
|
|
141
|
+
await new GctTerminalAdapter().fetchEvents({ containerNumber: 'A', config })
|
|
142
|
+
expect(fetchMock.mock.calls.filter(isAuthCall)).toHaveLength(1)
|
|
143
|
+
|
|
144
|
+
// 2nd fetchEvents: cache hit (no new token GET) + details POST that 401s
|
|
145
|
+
fetchMock.mockResolvedValueOnce(new Response('unauthorized', { status: 401 }))
|
|
146
|
+
await expect(
|
|
147
|
+
new GctTerminalAdapter().fetchEvents({ containerNumber: 'A', config }),
|
|
148
|
+
).rejects.toThrow(/401/)
|
|
149
|
+
expect(fetchMock.mock.calls.filter(isAuthCall)).toHaveLength(1) // still cached until invalidated
|
|
150
|
+
|
|
151
|
+
// 3rd fetchEvents: the 401 invalidated the cache, so a fresh token is minted
|
|
152
|
+
fetchMock.mockResolvedValueOnce(tokenResponse('t2'))
|
|
153
|
+
fetchMock.mockResolvedValueOnce(detailsResponse([]))
|
|
154
|
+
await new GctTerminalAdapter().fetchEvents({ containerNumber: 'A', config })
|
|
155
|
+
expect(fetchMock.mock.calls.filter(isAuthCall)).toHaveLength(2)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('GctTerminalAdapter token refresh (keep-alive)', () => {
|
|
160
|
+
// TC-TRACK-307 — an expired token is refreshed via /gctapi/ping, not re-minted.
|
|
161
|
+
it('refreshes an expired token via /gctapi/ping instead of re-authenticating', async () => {
|
|
162
|
+
const config = cfg()
|
|
163
|
+
// 1st fetchEvents mints a token that is ALREADY expired (past `expires`).
|
|
164
|
+
fetchMock.mockResolvedValueOnce(tokenResponse('t-mint', PAST))
|
|
165
|
+
fetchMock.mockResolvedValueOnce(detailsResponse([]))
|
|
166
|
+
await new GctTerminalAdapter().fetchEvents({ containerNumber: 'A', config })
|
|
167
|
+
|
|
168
|
+
// 2nd fetchEvents: token is stale → keep-alive refresh returns a fresh token.
|
|
169
|
+
fetchMock.mockResolvedValueOnce(tokenResponse('t-refreshed', FUTURE)) // ping
|
|
170
|
+
fetchMock.mockResolvedValueOnce(detailsResponse([]))
|
|
171
|
+
await new GctTerminalAdapter().fetchEvents({ containerNumber: 'A', config })
|
|
172
|
+
|
|
173
|
+
expect(fetchMock.mock.calls.filter(isRefreshCall)).toHaveLength(1)
|
|
174
|
+
expect(fetchMock.mock.calls.filter(isMintCall)).toHaveLength(1) // no re-auth
|
|
175
|
+
const details = fetchMock.mock.calls.filter(isDetailsCall)
|
|
176
|
+
expect((details[1][1] as any).headers.Authorization).toBe('t-refreshed')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// TC-TRACK-308 — a failed refresh falls back to a full re-authentication.
|
|
180
|
+
it('re-authenticates when the refresh call fails', async () => {
|
|
181
|
+
const config = cfg()
|
|
182
|
+
fetchMock.mockResolvedValueOnce(tokenResponse('t1', PAST)) // mint (already expired)
|
|
183
|
+
fetchMock.mockResolvedValueOnce(detailsResponse([]))
|
|
184
|
+
await new GctTerminalAdapter().fetchEvents({ containerNumber: 'A', config })
|
|
185
|
+
|
|
186
|
+
fetchMock.mockResolvedValueOnce(new Response('unauthorized', { status: 401 })) // ping fails
|
|
187
|
+
fetchMock.mockResolvedValueOnce(tokenResponse('t2', FUTURE)) // re-auth mint
|
|
188
|
+
fetchMock.mockResolvedValueOnce(detailsResponse([]))
|
|
189
|
+
await new GctTerminalAdapter().fetchEvents({ containerNumber: 'A', config })
|
|
190
|
+
|
|
191
|
+
expect(fetchMock.mock.calls.filter(isRefreshCall)).toHaveLength(1) // tried once
|
|
192
|
+
expect(fetchMock.mock.calls.filter(isMintCall)).toHaveLength(2) // initial + re-auth
|
|
193
|
+
const details = fetchMock.mock.calls.filter(isDetailsCall)
|
|
194
|
+
expect((details[1][1] as any).headers.Authorization).toBe('t2')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('re-authenticates when the refresh response carries no token', async () => {
|
|
198
|
+
const config = cfg()
|
|
199
|
+
fetchMock.mockResolvedValueOnce(tokenResponse('t1', PAST))
|
|
200
|
+
fetchMock.mockResolvedValueOnce(detailsResponse([]))
|
|
201
|
+
await new GctTerminalAdapter().fetchEvents({ containerNumber: 'A', config })
|
|
202
|
+
|
|
203
|
+
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ status: 'ok' }), { status: 200 })) // ping, no token
|
|
204
|
+
fetchMock.mockResolvedValueOnce(tokenResponse('t2', FUTURE))
|
|
205
|
+
fetchMock.mockResolvedValueOnce(detailsResponse([]))
|
|
206
|
+
await new GctTerminalAdapter().fetchEvents({ containerNumber: 'A', config })
|
|
207
|
+
|
|
208
|
+
expect(fetchMock.mock.calls.filter(isRefreshCall)).toHaveLength(1)
|
|
209
|
+
expect(fetchMock.mock.calls.filter(isMintCall)).toHaveLength(2)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('GctTerminalAdapter.fetchEventsBatch', () => {
|
|
214
|
+
it('loops per container and isolates a single container failure', async () => {
|
|
215
|
+
const config = cfg()
|
|
216
|
+
fetchMock.mockResolvedValueOnce(tokenResponse()) // up-front acquire
|
|
217
|
+
// container A (token cached now): details 500 → isolated
|
|
218
|
+
fetchMock.mockResolvedValueOnce(new Response('err', { status: 500 }))
|
|
219
|
+
// container B: details 200 with a mappable row
|
|
220
|
+
fetchMock.mockResolvedValueOnce(
|
|
221
|
+
detailsResponse([{ CntrID: 'B', VisitNo: 'VB', GroundingDateTime: '2026-08-10T09:30:00Z' }]),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
const { events } = await new GctTerminalAdapter().fetchEventsBatch({
|
|
225
|
+
containerNumbers: ['A', 'B'],
|
|
226
|
+
config,
|
|
227
|
+
})
|
|
228
|
+
expect(events).toHaveLength(1)
|
|
229
|
+
expect(events[0].containerNumber).toBe('B')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('fails the whole batch when the up-front token acquire fails', async () => {
|
|
233
|
+
fetchMock.mockResolvedValueOnce(new Response('nope', { status: 403 }))
|
|
234
|
+
await expect(
|
|
235
|
+
new GctTerminalAdapter().fetchEventsBatch({ containerNumbers: ['A', 'B'], config: cfg() }),
|
|
236
|
+
).rejects.toThrow()
|
|
237
|
+
})
|
|
238
|
+
})
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mapContainerToEvents, directionFromStatus, parseGctDateTime } from '../gct-semantics'
|
|
3
|
+
import type { GctContainer } from '../types'
|
|
4
|
+
import type { ResolvedTerminalConfig } from '../../../terminal-adapter'
|
|
5
|
+
|
|
6
|
+
const config: ResolvedTerminalConfig = {
|
|
7
|
+
terminalCode: 'gct',
|
|
8
|
+
adapterType: 'gct',
|
|
9
|
+
displayName: 'Gdynia Container Terminal',
|
|
10
|
+
baseUrl: 'https://api.gct.example',
|
|
11
|
+
endpoints: { unit: '/gctapi/GetContainerDetails' },
|
|
12
|
+
authType: 'gct_token',
|
|
13
|
+
rateLimitRequests: 60,
|
|
14
|
+
rateLimitWindowSeconds: 60,
|
|
15
|
+
unlocode: 'PLGDY',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function container(partial: Partial<GctContainer>): GctContainer {
|
|
19
|
+
return { CntrID: 'GCTU1234567', VisitNo: 'V-001', ...partial }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('directionFromStatus', () => {
|
|
23
|
+
it('maps X* to export and I* to import', () => {
|
|
24
|
+
expect(directionFromStatus('XF')).toBe('export')
|
|
25
|
+
expect(directionFromStatus('XM')).toBe('export')
|
|
26
|
+
expect(directionFromStatus('IF')).toBe('import')
|
|
27
|
+
})
|
|
28
|
+
it('returns null for empty-no-purpose, unknown, and blank', () => {
|
|
29
|
+
expect(directionFromStatus('EM')).toBeNull()
|
|
30
|
+
expect(directionFromStatus('ZZ')).toBeNull()
|
|
31
|
+
expect(directionFromStatus('')).toBeNull()
|
|
32
|
+
expect(directionFromStatus(null)).toBeNull()
|
|
33
|
+
})
|
|
34
|
+
it('does not crash on the undocumented XI status', () => {
|
|
35
|
+
expect(directionFromStatus('XI')).toBe('export')
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('parseGctDateTime', () => {
|
|
40
|
+
it('parses ISO-8601', () => {
|
|
41
|
+
expect(parseGctDateTime('2026-08-10T09:30:00Z')?.toISOString()).toBe('2026-08-10T09:30:00.000Z')
|
|
42
|
+
})
|
|
43
|
+
it('parses the N4-style "yyyy-mm-dd hh:mm" as terminal-local (Europe/Warsaw) → UTC', () => {
|
|
44
|
+
// 09:30 CEST (+02:00) on 2026-08-10 is 07:30 UTC.
|
|
45
|
+
expect(parseGctDateTime('2026-08-10 09:30')?.toISOString()).toBe('2026-08-10T07:30:00.000Z')
|
|
46
|
+
})
|
|
47
|
+
it('returns null for empty/garbage', () => {
|
|
48
|
+
expect(parseGctDateTime(null)).toBeNull()
|
|
49
|
+
expect(parseGctDateTime(' ')).toBeNull()
|
|
50
|
+
expect(parseGctDateTime('not-a-date')).toBeNull()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('mapContainerToEvents', () => {
|
|
55
|
+
// TC-TRACK-302
|
|
56
|
+
it('emits one gate_in (GTIN) event from GroundingDateTime, keyed on VisitNo', () => {
|
|
57
|
+
const events = mapContainerToEvents(
|
|
58
|
+
container({ GroundingDateTime: '2026-08-10T09:30:00Z', CntrStatus: 'IF', VGMWeight: 24000 }),
|
|
59
|
+
config,
|
|
60
|
+
)
|
|
61
|
+
expect(events).toHaveLength(1)
|
|
62
|
+
const ev = events[0]
|
|
63
|
+
expect(ev.eventType).toBe('EQUIPMENT')
|
|
64
|
+
expect(ev.eventCode).toBe('GTIN')
|
|
65
|
+
expect(ev.eventClassifierCode).toBe('ACT')
|
|
66
|
+
expect(ev.ufvGkey).toBe('V-001')
|
|
67
|
+
expect(ev.sourceEventId).toBe('gct:V-001:GTIN')
|
|
68
|
+
expect(ev.containerNumber).toBe('GCTU1234567')
|
|
69
|
+
expect(ev.unlocode).toBe('PLGDY')
|
|
70
|
+
expect(ev.vgmWeightKg).toBe(24000)
|
|
71
|
+
expect(ev.transitState).toBe('IF')
|
|
72
|
+
expect(ev.eventDateTime.toISOString()).toBe('2026-08-10T09:30:00.000Z')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// TC-TRACK-303
|
|
76
|
+
it('adds a departed (DEPA) event when PickupDateTime is present', () => {
|
|
77
|
+
const events = mapContainerToEvents(
|
|
78
|
+
container({ GroundingDateTime: '2026-08-10T09:30:00Z', PickupDateTime: '2026-08-12T14:00:00Z' }),
|
|
79
|
+
config,
|
|
80
|
+
)
|
|
81
|
+
expect(events.map((e) => e.eventCode)).toEqual(['GTIN', 'DEPA'])
|
|
82
|
+
const dep = events.find((e) => e.eventCode === 'DEPA')!
|
|
83
|
+
expect(dep.eventType).toBe('TRANSPORT')
|
|
84
|
+
expect(dep.sourceEventId).toBe('gct:V-001:DEPA')
|
|
85
|
+
expect(dep.eventDateTime.toISOString()).toBe('2026-08-12T14:00:00.000Z')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('emits no events when neither timestamp is present', () => {
|
|
89
|
+
expect(mapContainerToEvents(container({ CntrStatus: 'XF' }), config)).toEqual([])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// TC-TRACK-304
|
|
93
|
+
it('passes HoldCodesList through as impediments (dropping blanks)', () => {
|
|
94
|
+
const events = mapContainerToEvents(
|
|
95
|
+
container({ GroundingDateTime: '2026-08-10T09:30:00Z', HoldCodesList: ['CUSTOMS', '', ' ', 'VGM'] }),
|
|
96
|
+
config,
|
|
97
|
+
)
|
|
98
|
+
expect(events[0].impediments).toEqual(['CUSTOMS', 'VGM'])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('maps SealList to seal objects and preserves the full snapshot in rawData', () => {
|
|
102
|
+
const events = mapContainerToEvents(
|
|
103
|
+
container({
|
|
104
|
+
GroundingDateTime: '2026-08-10T09:30:00Z',
|
|
105
|
+
SealList: ['SEAL1'],
|
|
106
|
+
CntrStatus: 'XF',
|
|
107
|
+
VesselName: 'MSC ISABELLA',
|
|
108
|
+
GCTVoyage: 'V123',
|
|
109
|
+
}),
|
|
110
|
+
config,
|
|
111
|
+
)
|
|
112
|
+
const ev = events[0]
|
|
113
|
+
expect(ev.seals).toEqual([{ number: 'SEAL1', source: 'gct' }])
|
|
114
|
+
expect(ev.vesselName).toBe('MSC ISABELLA')
|
|
115
|
+
expect(ev.voyageNumber).toBe('V123')
|
|
116
|
+
expect(ev.modeOfTransport).toBe('VESSEL')
|
|
117
|
+
expect((ev.rawData as Record<string, unknown>).Category).toBe('export')
|
|
118
|
+
expect((ev.rawData as Record<string, unknown>).CntrID).toBe('GCTU1234567')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('falls back to CntrID for the dedup key when VisitNo is missing', () => {
|
|
122
|
+
const events = mapContainerToEvents(
|
|
123
|
+
container({ VisitNo: null, GroundingDateTime: '2026-08-10T09:30:00Z' }),
|
|
124
|
+
config,
|
|
125
|
+
)
|
|
126
|
+
expect(events[0].ufvGkey).toBe('GCTU1234567')
|
|
127
|
+
expect(events[0].sourceEventId).toBe('gct:GCTU1234567:GTIN')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('returns [] for a container with no CntrID', () => {
|
|
131
|
+
expect(mapContainerToEvents({ CntrID: '' } as GctContainer, config)).toEqual([])
|
|
132
|
+
})
|
|
133
|
+
})
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TerminalAdapter,
|
|
3
|
+
TerminalFetchResult,
|
|
4
|
+
TerminalFetchedEvent,
|
|
5
|
+
TerminalAdapterTestResult,
|
|
6
|
+
ResolvedTerminalConfig,
|
|
7
|
+
} from '../../terminal-adapter'
|
|
8
|
+
import type { GctContainer, GctContainerDetailsResponse } from './types'
|
|
9
|
+
import { acquireGctToken, invalidateGctToken, GctAuthError } from './auth/token'
|
|
10
|
+
import { mapContainerToEvents } from './gct-semantics'
|
|
11
|
+
import { n4Request } from '../n4/http'
|
|
12
|
+
import { terminalLogger } from '../../logger'
|
|
13
|
+
|
|
14
|
+
function joinUrl(base: string, path: string): string {
|
|
15
|
+
return `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Config-driven adapter for the GCT (Gdynia Container Terminal) API. One
|
|
20
|
+
* instance serves every GCT terminal; per-terminal differences live in the
|
|
21
|
+
* TerminalConfig.
|
|
22
|
+
*
|
|
23
|
+
* Differences from the N4 adapter that shape this code:
|
|
24
|
+
* - Auth is a path-minted token replayed in the `Authorization` header
|
|
25
|
+
* (see ./auth/token.ts), not an OAuth2 Bearer grant.
|
|
26
|
+
* - There is no fetch-many-by-container-number endpoint, so `fetchEventsBatch`
|
|
27
|
+
* simply loops `fetchEvents` (bounded upstream by the per-terminal limiter).
|
|
28
|
+
* - There is no vessel endpoint, so `fetchVesselVisit` is intentionally omitted
|
|
29
|
+
* — the service feature-detects it and skips vessel resolution for GCT.
|
|
30
|
+
*/
|
|
31
|
+
export class GctTerminalAdapter implements TerminalAdapter {
|
|
32
|
+
readonly adapterType = 'gct'
|
|
33
|
+
|
|
34
|
+
readonly defaultEndpoints = {
|
|
35
|
+
// GCT's container-details endpoint occupies the generic `unit` slot.
|
|
36
|
+
unit: '/gctapi/GetContainerDetails',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private detailsUrl(config: ResolvedTerminalConfig): string {
|
|
40
|
+
const path = config.endpoints?.unit?.trim() || this.defaultEndpoints.unit
|
|
41
|
+
return joinUrl(config.baseUrl, path)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async getContainerDetails(
|
|
45
|
+
containerNumber: string,
|
|
46
|
+
config: ResolvedTerminalConfig,
|
|
47
|
+
token: string,
|
|
48
|
+
): Promise<GctContainer[]> {
|
|
49
|
+
const { status, ok, text } = await n4Request(
|
|
50
|
+
this.detailsUrl(config),
|
|
51
|
+
{
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: token,
|
|
55
|
+
Accept: 'application/json',
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify({ CntrID: containerNumber, pageno: 1, pagesize: 100 }),
|
|
59
|
+
},
|
|
60
|
+
{ proxyUrl: config.proxyUrl, label: `${config.terminalCode} GetContainerDetails` },
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if (status === 401) {
|
|
64
|
+
invalidateGctToken(config)
|
|
65
|
+
throw new GctAuthError(`GetContainerDetails unauthorized (401) for ${config.terminalCode}`)
|
|
66
|
+
}
|
|
67
|
+
if (!ok) {
|
|
68
|
+
throw new Error(`GetContainerDetails failed (${status}) for ${config.terminalCode}`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let parsed: GctContainerDetailsResponse
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(text) as GctContainerDetailsResponse
|
|
74
|
+
} catch {
|
|
75
|
+
throw new Error(`GetContainerDetails response was not valid JSON for ${config.terminalCode}`)
|
|
76
|
+
}
|
|
77
|
+
return Array.isArray(parsed.Containers) ? parsed.Containers : []
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async fetchEvents(input: {
|
|
81
|
+
containerNumber: string
|
|
82
|
+
config: ResolvedTerminalConfig
|
|
83
|
+
}): Promise<TerminalFetchResult> {
|
|
84
|
+
const { containerNumber, config } = input
|
|
85
|
+
const token = await acquireGctToken(config)
|
|
86
|
+
const containers = await this.getContainerDetails(containerNumber, config, token)
|
|
87
|
+
|
|
88
|
+
const events: TerminalFetchedEvent[] = []
|
|
89
|
+
for (const container of containers) {
|
|
90
|
+
events.push(...mapContainerToEvents(container, config))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
terminalLogger.debug('gct fetchEvents', {
|
|
94
|
+
terminalCode: config.terminalCode,
|
|
95
|
+
container: containerNumber,
|
|
96
|
+
rows: containers.length,
|
|
97
|
+
events: events.length,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return { events, containerNumber }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* GCT has no batch-by-container-number call, so fetch each container in turn.
|
|
105
|
+
* Kept as a distinct method (rather than falling through to per-container in
|
|
106
|
+
* the service) so the scheduler's batch path stays a single call site; the
|
|
107
|
+
* per-terminal rate limiter still throttles the underlying requests. A single
|
|
108
|
+
* container's failure is isolated so one bad number can't sink the whole poll.
|
|
109
|
+
*/
|
|
110
|
+
async fetchEventsBatch(input: {
|
|
111
|
+
containerNumbers: string[]
|
|
112
|
+
config: ResolvedTerminalConfig
|
|
113
|
+
}): Promise<{ events: TerminalFetchedEvent[] }> {
|
|
114
|
+
const { containerNumbers, config } = input
|
|
115
|
+
if (containerNumbers.length === 0) return { events: [] }
|
|
116
|
+
|
|
117
|
+
// Acquire once up front so a genuine auth failure fails the whole batch
|
|
118
|
+
// (marking the job failed) instead of being swallowed 100× as per-container
|
|
119
|
+
// data errors below.
|
|
120
|
+
await acquireGctToken(config)
|
|
121
|
+
|
|
122
|
+
const events: TerminalFetchedEvent[] = []
|
|
123
|
+
for (const containerNumber of containerNumbers) {
|
|
124
|
+
try {
|
|
125
|
+
const result = await this.fetchEvents({ containerNumber, config })
|
|
126
|
+
events.push(...result.events)
|
|
127
|
+
} catch (err) {
|
|
128
|
+
// An auth error is fatal to the batch (token revoked/expired mid-run);
|
|
129
|
+
// let it propagate. Data/transport errors are isolated so one bad
|
|
130
|
+
// container can't sink the poll for the rest.
|
|
131
|
+
if (err instanceof GctAuthError) throw err
|
|
132
|
+
terminalLogger.warn('gct fetchEventsBatch: container failed', {
|
|
133
|
+
terminalCode: config.terminalCode,
|
|
134
|
+
container: containerNumber,
|
|
135
|
+
message: err instanceof Error ? err.message : String(err),
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { events }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async testConnection(config: ResolvedTerminalConfig): Promise<TerminalAdapterTestResult> {
|
|
143
|
+
const started = Date.now()
|
|
144
|
+
try {
|
|
145
|
+
await acquireGctToken(config)
|
|
146
|
+
return { success: true, message: 'Token acquired', latencyMs: Date.now() - started }
|
|
147
|
+
} catch (err) {
|
|
148
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
149
|
+
return { success: false, message, latencyMs: Date.now() - started }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|