@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,206 @@
|
|
|
1
|
+
{
|
|
2
|
+
"terminal_tracking": {
|
|
3
|
+
"title": "Śledzenie terminali",
|
|
4
|
+
"stops": {
|
|
5
|
+
"title": "Ograniczenia załadunku",
|
|
6
|
+
"vsl": "Brak załadunku na statek",
|
|
7
|
+
"road": "Brak podjęcia drogowego",
|
|
8
|
+
"rail": "Brak podjęcia kolejowego"
|
|
9
|
+
},
|
|
10
|
+
"nav": {
|
|
11
|
+
"group": "Śledzenie terminali",
|
|
12
|
+
"terminal_configs": "Terminale",
|
|
13
|
+
"tracking_jobs": "Śledzone kontenery"
|
|
14
|
+
},
|
|
15
|
+
"errors": {
|
|
16
|
+
"tenant_required": "Wymagany jest kontekst najemcy",
|
|
17
|
+
"organization_required": "Wymagany jest kontekst organizacji"
|
|
18
|
+
},
|
|
19
|
+
"terminal_config": {
|
|
20
|
+
"title": "Konfiguracje terminali",
|
|
21
|
+
"displayName": "Nazwa",
|
|
22
|
+
"terminalCode": "Kod terminalu",
|
|
23
|
+
"adapterType": "Adapter",
|
|
24
|
+
"baseUrl": "Bazowy adres API",
|
|
25
|
+
"proxyUrl": "Adres proxy API (opcjonalnie)",
|
|
26
|
+
"unlocode": "UN/LOCODE",
|
|
27
|
+
"smdgCodes": "Kody SMDG (po przecinku)",
|
|
28
|
+
"bicCodes": "Kody BIC (po przecinku)",
|
|
29
|
+
"nameAliases": "Aliasy nazwy (po przecinku)",
|
|
30
|
+
"tokenUrl": "Adres URL tokenu",
|
|
31
|
+
"scope": "Zakres (scope)",
|
|
32
|
+
"clientId": "Identyfikator klienta",
|
|
33
|
+
"rateLimitRequests": "Limit zapytań",
|
|
34
|
+
"rateLimitWindowSeconds": "Okno (sekundy)",
|
|
35
|
+
"isActive": "Aktywny",
|
|
36
|
+
"unchanged": "bez zmian",
|
|
37
|
+
"credEditHint": "Pozostaw dane uwierzytelniające puste, aby ich nie zmieniać.",
|
|
38
|
+
"gctHint": "Klucz (base32) widoczny w panelu GCT obok kodu QR. Na jego podstawie przy każdym logowaniu obliczany jest aktualny kod jednorazowy.",
|
|
39
|
+
"section": {
|
|
40
|
+
"terminal": "Terminal",
|
|
41
|
+
"auth": "Uwierzytelnianie",
|
|
42
|
+
"matching": "Dopasowanie",
|
|
43
|
+
"advanced": "Zaawansowane"
|
|
44
|
+
},
|
|
45
|
+
"cred": {
|
|
46
|
+
"username": "Nazwa użytkownika",
|
|
47
|
+
"password": "Hasło",
|
|
48
|
+
"clientSecret": "Sekret klienta",
|
|
49
|
+
"companyCode": "Kod firmy",
|
|
50
|
+
"loginName": "Login",
|
|
51
|
+
"totpSecret": "Twój sekret 2FA"
|
|
52
|
+
},
|
|
53
|
+
"validation": {
|
|
54
|
+
"allCredentials": "Wprowadź wszystkie pola danych uwierzytelniających",
|
|
55
|
+
"credentialsRequired": "Dane uwierzytelniające są wymagane"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"tracking_job": {
|
|
59
|
+
"title": "Zadania śledzenia terminali",
|
|
60
|
+
"containerNumber": "Kontener",
|
|
61
|
+
"status": "Status",
|
|
62
|
+
"gateIn": "Wjazd",
|
|
63
|
+
"gateOut": "Wyjazd",
|
|
64
|
+
"holds": "Blokady",
|
|
65
|
+
"lastPollAt": "Ostatnie odpytanie",
|
|
66
|
+
"details": "Szczegóły kontenera",
|
|
67
|
+
"noEvents": "Brak zdarzeń terminalowych. Użyj „Odpytaj teraz”, aby pobrać.",
|
|
68
|
+
"leg": "Wizyta na terminalu",
|
|
69
|
+
"recentErrors": "Ostatnie błędy odpytania",
|
|
70
|
+
"jobInfo": "Śledzenie",
|
|
71
|
+
"nextPollAt": "Następne odpytanie",
|
|
72
|
+
"retryCount": "Ponowienia",
|
|
73
|
+
"jobId": "ID zadania",
|
|
74
|
+
"actions": {
|
|
75
|
+
"details": "Szczegóły",
|
|
76
|
+
"poll": "Odpytaj teraz"
|
|
77
|
+
},
|
|
78
|
+
"flash": {
|
|
79
|
+
"polled": "Odpytano — {n} nowych, {u} zaktualizowanych zdarzeń"
|
|
80
|
+
},
|
|
81
|
+
"statuses": {
|
|
82
|
+
"active": "Aktywny",
|
|
83
|
+
"paused": "Wstrzymany",
|
|
84
|
+
"deactivated": "Dezaktywowany",
|
|
85
|
+
"failed": "Błąd",
|
|
86
|
+
"completed": "Zakończony"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"event": {
|
|
90
|
+
"source_badge": "Terminal",
|
|
91
|
+
"yardPosition": "Pozycja na placu",
|
|
92
|
+
"vgm": "VGM (kg)",
|
|
93
|
+
"visitPhase": "Faza wizyty",
|
|
94
|
+
"impediments": "Blokady",
|
|
95
|
+
"seals": "Plomby",
|
|
96
|
+
"eta": "ETA",
|
|
97
|
+
"etd": "ETD",
|
|
98
|
+
"ata": "ATA",
|
|
99
|
+
"atd": "ATD",
|
|
100
|
+
"gate_in": "Wjazd (brama)",
|
|
101
|
+
"yard": "Na placu / rozładowany",
|
|
102
|
+
"loaded": "Załadowany",
|
|
103
|
+
"loadedAt": "Data załadunku",
|
|
104
|
+
"departed": "Wyjazd",
|
|
105
|
+
"ufvGkey": "Ufv_Gkey",
|
|
106
|
+
"rawData": "Surowe dane terminalu"
|
|
107
|
+
},
|
|
108
|
+
"holds": {
|
|
109
|
+
"unknown": {
|
|
110
|
+
"label": "Nieznana blokada",
|
|
111
|
+
"description": "Nierozpoznana blokada terminalowa. Szczegóły uzyskasz w terminalu.",
|
|
112
|
+
"resolution": "Skontaktuj się z terminalem, aby ustalić sposób zdjęcia blokady."
|
|
113
|
+
},
|
|
114
|
+
"cfs_appointment": {
|
|
115
|
+
"label": "Awizacja CFS",
|
|
116
|
+
"description": "Nakładana podczas przyjmowania zlecenia importowego przez magazyn CFS.",
|
|
117
|
+
"resolution": "Zdejmowana automatycznie po zrealizowaniu zlecenia i pracy magazynu."
|
|
118
|
+
},
|
|
119
|
+
"customs_import_permission": {
|
|
120
|
+
"label": "Pozwolenie celne (import)",
|
|
121
|
+
"description": "Nakładana systemowo na wszystkie kontenery importowe oczekujące na zwolnienie celne.",
|
|
122
|
+
"resolution": "Zdejmowana automatycznie po aktualizacji numeru MRN przy użyciu polskich kodów PCS."
|
|
123
|
+
},
|
|
124
|
+
"customs_import_hold": {
|
|
125
|
+
"label": "Blokada celna (import)",
|
|
126
|
+
"description": "Nakładana ręcznie przez funkcjonariusza celnego w celu skanowania, kontroli lub z powodu problemu z odprawą.",
|
|
127
|
+
"resolution": "Może ją zdjąć wyłącznie funkcjonariusz celny — skontaktuj się z urzędem celnym."
|
|
128
|
+
},
|
|
129
|
+
"line_import_permission": {
|
|
130
|
+
"label": "Pozwolenie armatora (import)",
|
|
131
|
+
"description": "Nakładana systemowo na wszystkie kontenery importowe do czasu zwolnienia przez armatora.",
|
|
132
|
+
"resolution": "Zdejmowana po zwolnieniu kontenera przez armatora numerem PIN."
|
|
133
|
+
},
|
|
134
|
+
"oog_hold": {
|
|
135
|
+
"label": "Blokada OOG",
|
|
136
|
+
"description": "Nakładana na ponadgabarytowe kontenery importowe wymagające specjalnych zezwoleń.",
|
|
137
|
+
"resolution": "Wymaga weryfikacji przez Baltic Hub przed odbiorem; do tego czasu kontener nie może zostać awizowany do odbioru."
|
|
138
|
+
},
|
|
139
|
+
"cfs_hold": {
|
|
140
|
+
"label": "Blokada CFS",
|
|
141
|
+
"description": "Nakładana podczas przyjmowania zlecenia eksportowego przez magazyn CFS.",
|
|
142
|
+
"resolution": "Zdejmowana automatycznie po zrealizowaniu zlecenia i pracy magazynu."
|
|
143
|
+
},
|
|
144
|
+
"customs_export_permission": {
|
|
145
|
+
"label": "Pozwolenie celne (eksport)",
|
|
146
|
+
"description": "Nakładana systemowo na kontenery eksportowe oczekujące na zwolnienie celne.",
|
|
147
|
+
"resolution": "Zdejmowana automatycznie po aktualizacji MRN; musi zostać zdjęta przed terminem odprawy celnej."
|
|
148
|
+
},
|
|
149
|
+
"customs_export_hold": {
|
|
150
|
+
"label": "Blokada celna (eksport)",
|
|
151
|
+
"description": "Nakładana ręcznie przez funkcjonariusza celnego w celu skanowania, kontroli lub z powodu problemu z odprawą.",
|
|
152
|
+
"resolution": "Może ją zdjąć wyłącznie funkcjonariusz celny — skontaktuj się z urzędem celnym."
|
|
153
|
+
},
|
|
154
|
+
"unit_vgm_load_permission": {
|
|
155
|
+
"label": "Pozwolenie VGM na załadunek",
|
|
156
|
+
"description": "Nakładana na wszystkie pełne kontenery eksportowe bez zweryfikowanej masy brutto (VGM).",
|
|
157
|
+
"resolution": "Zdejmowana po wprowadzeniu masy VGM do systemu Navis."
|
|
158
|
+
},
|
|
159
|
+
"dgd_hold": {
|
|
160
|
+
"label": "Blokada DGD",
|
|
161
|
+
"description": "Nakładana automatycznie na wszystkie kontenery eksportowe z ładunkiem IMO (niebezpiecznym).",
|
|
162
|
+
"resolution": "Wymaga złożenia dokumentu DGD (deklaracji ładunku niebezpiecznego)."
|
|
163
|
+
},
|
|
164
|
+
"scanning_hold": {
|
|
165
|
+
"label": "Blokada skanowania",
|
|
166
|
+
"description": "Nakładana ręcznie przez funkcjonariusza celnego; kontener musi zostać przedstawiony do skanowania.",
|
|
167
|
+
"resolution": "Zdejmowana przez służby celne po skanowaniu (wybór DCT_CFS_SCANNING)."
|
|
168
|
+
},
|
|
169
|
+
"empty_permission": {
|
|
170
|
+
"label": "Pozwolenie (pusty)",
|
|
171
|
+
"description": "Nakładana systemowo na wszystkie puste kontenery oczekujące na referencję zwolnienia.",
|
|
172
|
+
"resolution": "Zdejmowana po podaniu numeru EDO / bookingu od armatora."
|
|
173
|
+
},
|
|
174
|
+
"missing_agent_permission": {
|
|
175
|
+
"label": "Brak pozwolenia agenta",
|
|
176
|
+
"description": "Dotyczy nieaktywnej płatności PCS. Obecnie nie blokuje załadunku na statek ani odbioru.",
|
|
177
|
+
"resolution": "Zdejmowana po wpisaniu kodu armatora w polu Agent One."
|
|
178
|
+
},
|
|
179
|
+
"technical_full_container": {
|
|
180
|
+
"label": "Techniczna (pełny kontener)",
|
|
181
|
+
"description": "Nakładana na wszystkie pełne kontenery importowe/eksportowe w celu obsługi rozpoznawania znaków OCR. Nie blokuje ruchu.",
|
|
182
|
+
"resolution": "Nie wymaga działań — wyłącznie informacyjna."
|
|
183
|
+
},
|
|
184
|
+
"technical_live_reefer": {
|
|
185
|
+
"label": "Techniczna (aktywny reefer)",
|
|
186
|
+
"description": "Nakładana na wszystkie pełne kontenery chłodnicze w celu obsługi OCR. Nie blokuje operacji.",
|
|
187
|
+
"resolution": "Nie wymaga działań — wyłącznie informacyjna."
|
|
188
|
+
},
|
|
189
|
+
"terminal_hold": {
|
|
190
|
+
"label": "Blokada terminalowa",
|
|
191
|
+
"description": "Nakładana z powodu nieprawidłowości kontenera (błędne oznakowanie, wygięte belki kontenera itp.).",
|
|
192
|
+
"resolution": "Skontaktuj się z Biurem Obsługi Klienta Baltic Hub."
|
|
193
|
+
},
|
|
194
|
+
"czrm_hold": {
|
|
195
|
+
"label": "Blokada CZRM",
|
|
196
|
+
"description": "Nakładana przez urząd celny.",
|
|
197
|
+
"resolution": "Skontaktuj się bezpośrednio z urzędem celnym w celu jej zdjęcia."
|
|
198
|
+
},
|
|
199
|
+
"izrx_hold": {
|
|
200
|
+
"label": "Blokada IZRX",
|
|
201
|
+
"description": "Nakładana przez urząd celny.",
|
|
202
|
+
"resolution": "Skontaktuj się bezpośrednio z urzędem celnym w celu jej zdjęcia."
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ModuleInfo } from '@open-mercato/shared/modules/registry'
|
|
2
|
+
import './commands'
|
|
3
|
+
|
|
4
|
+
export const metadata: ModuleInfo = {
|
|
5
|
+
name: 'terminal_tracking',
|
|
6
|
+
title: 'Terminal Tracking',
|
|
7
|
+
version: '0.1.0',
|
|
8
|
+
description: 'Container terminal (Navis N4) tracking with per-terminal config, periodic polling, and events.',
|
|
9
|
+
author: 'FreightTech',
|
|
10
|
+
license: 'Proprietary',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { features } from './acl'
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
blockingHoldCount,
|
|
4
|
+
impedimentsChanged,
|
|
5
|
+
isEmptyReady,
|
|
6
|
+
isImportHoldsCleared,
|
|
7
|
+
type AvailabilityInput,
|
|
8
|
+
} from '../availability'
|
|
9
|
+
|
|
10
|
+
// Catalogue references (see ../holds.ts):
|
|
11
|
+
// - blocking: CUSTOMS IMPORT PERMISSION, LINE IMPORT PERMISSION, EMPTY PERMISSION
|
|
12
|
+
// - non-blocking: TECHNICAL FULL CONTAINER, MISSING AGENT PERMISSION
|
|
13
|
+
const BLOCKING = '!CUSTOMS IMPORT PERMISSION'
|
|
14
|
+
const BLOCKING_2 = '!LINE IMPORT PERMISSION'
|
|
15
|
+
const EMPTY_PERMISSION = '!EMPTY PERMISSION'
|
|
16
|
+
const NON_BLOCKING = '!TECHNICAL_FULL_CONTAINER'
|
|
17
|
+
|
|
18
|
+
function evt(
|
|
19
|
+
category: string,
|
|
20
|
+
frghtKind: 'MTY' | 'Empty' | 'FCL',
|
|
21
|
+
transitState: string,
|
|
22
|
+
impediments: string[] | null,
|
|
23
|
+
extraRaw: Record<string, unknown> = {},
|
|
24
|
+
): AvailabilityInput {
|
|
25
|
+
return {
|
|
26
|
+
transitState,
|
|
27
|
+
impediments,
|
|
28
|
+
rawData: { Category: category, 'Frght Kind': frghtKind, ...extraRaw },
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('blockingHoldCount', () => {
|
|
33
|
+
it('counts only movement-blocking holds', () => {
|
|
34
|
+
expect(blockingHoldCount([BLOCKING, NON_BLOCKING])).toBe(1)
|
|
35
|
+
expect(blockingHoldCount([BLOCKING, BLOCKING_2])).toBe(2)
|
|
36
|
+
expect(blockingHoldCount([NON_BLOCKING])).toBe(0)
|
|
37
|
+
expect(blockingHoldCount([])).toBe(0)
|
|
38
|
+
expect(blockingHoldCount(null)).toBe(0)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('isEmptyReady', () => {
|
|
43
|
+
it('fires for an empty in the yard with no blocking holds', () => {
|
|
44
|
+
expect(isEmptyReady(evt('Export', 'MTY', 'S40_YARD', null))).toBe(true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('recognises the real Baltic Hub empty value "Empty" (not just "MTY")', () => {
|
|
48
|
+
expect(isEmptyReady(evt('Storage', 'Empty', 'S40_YARD', null))).toBe(true)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('fires regardless of category (empties sit under N4 "Storage")', () => {
|
|
52
|
+
expect(isEmptyReady(evt('Storage', 'MTY', 'S40_YARD', null))).toBe(true)
|
|
53
|
+
expect(isEmptyReady(evt('Import', 'MTY', 'S40_YARD', null))).toBe(true)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('fires when only non-blocking holds are present (incl. the real REFFER spelling)', () => {
|
|
57
|
+
expect(isEmptyReady(evt('Storage', 'MTY', 'S40_YARD', [NON_BLOCKING]))).toBe(true)
|
|
58
|
+
expect(isEmptyReady(evt('Storage', 'MTY', 'S40_YARD', ['!TECHNICAL LIVE REFFER']))).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('does not fire while EMPTY PERMISSION (or any blocking hold) is present', () => {
|
|
62
|
+
expect(isEmptyReady(evt('Storage', 'MTY', 'S40_YARD', [EMPTY_PERMISSION]))).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('does not fire for a full container', () => {
|
|
66
|
+
expect(isEmptyReady(evt('Export', 'FCL', 'S40_YARD', null))).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('does not fire when not yet in the yard', () => {
|
|
70
|
+
expect(isEmptyReady(evt('Storage', 'MTY', 'S20_INBOUND', null))).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('does not fire while road pickup is explicitly stopped (Stop-Road)', () => {
|
|
74
|
+
expect(isEmptyReady(evt('Storage', 'MTY', 'S40_YARD', null, { 'Stop-Road': 'Y' }))).toBe(false)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('still fires when Stop-Road is present but not active, or an unrelated stop is set', () => {
|
|
78
|
+
expect(isEmptyReady(evt('Storage', 'MTY', 'S40_YARD', null, { 'Stop-Road': 'N' }))).toBe(true)
|
|
79
|
+
expect(isEmptyReady(evt('Storage', 'MTY', 'S40_YARD', null, { 'Stop-Vsl': 'Y' }))).toBe(true)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('isImportHoldsCleared', () => {
|
|
84
|
+
it('fires on the blocked → clear transition for an import in the yard', () => {
|
|
85
|
+
const e = evt('Import', 'FCL', 'S40_YARD', [])
|
|
86
|
+
expect(isImportHoldsCleared(e, [BLOCKING])).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('does not fire for a container that was never blocked', () => {
|
|
90
|
+
const e = evt('Import', 'FCL', 'S40_YARD', [])
|
|
91
|
+
expect(isImportHoldsCleared(e, [])).toBe(false)
|
|
92
|
+
expect(isImportHoldsCleared(e, [NON_BLOCKING])).toBe(false)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('does not fire while a blocking hold remains', () => {
|
|
96
|
+
const e = evt('Import', 'FCL', 'S40_YARD', [BLOCKING_2])
|
|
97
|
+
expect(isImportHoldsCleared(e, [BLOCKING])).toBe(false)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('does not fire when not in the yard', () => {
|
|
101
|
+
const e = evt('Import', 'FCL', 'S20_INBOUND', [])
|
|
102
|
+
expect(isImportHoldsCleared(e, [BLOCKING])).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('does not fire for an export leg', () => {
|
|
106
|
+
const e = evt('Export', 'FCL', 'S40_YARD', [])
|
|
107
|
+
expect(isImportHoldsCleared(e, [BLOCKING])).toBe(false)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('still fires under a road stop — the transition event is not gated (would be lost, not delayed)', () => {
|
|
111
|
+
const e = evt('Import', 'FCL', 'S40_YARD', [], { 'Stop-Road': 'Y' })
|
|
112
|
+
expect(isImportHoldsCleared(e, [BLOCKING])).toBe(true)
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('impedimentsChanged', () => {
|
|
117
|
+
it('detects an added hold', () => {
|
|
118
|
+
expect(impedimentsChanged([BLOCKING], [BLOCKING, BLOCKING_2])).toBe(true)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('detects a removed hold', () => {
|
|
122
|
+
expect(impedimentsChanged([BLOCKING, BLOCKING_2], [BLOCKING])).toBe(true)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('treats a null/absent prior with current holds as a change (first appearance)', () => {
|
|
126
|
+
expect(impedimentsChanged(null, [BLOCKING])).toBe(true)
|
|
127
|
+
expect(impedimentsChanged(undefined, [BLOCKING])).toBe(true)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('is false when nothing changed (order- and duplicate-insensitive)', () => {
|
|
131
|
+
expect(impedimentsChanged([BLOCKING, BLOCKING_2], [BLOCKING_2, BLOCKING])).toBe(false)
|
|
132
|
+
expect(impedimentsChanged([BLOCKING, BLOCKING], [BLOCKING])).toBe(false)
|
|
133
|
+
expect(impedimentsChanged(null, [])).toBe(false)
|
|
134
|
+
expect(impedimentsChanged([], null)).toBe(false)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { ensureTerminalPollSchedule } from '../ensure-schedule'
|
|
3
|
+
|
|
4
|
+
const scope = {
|
|
5
|
+
organizationId: '942dbb1e-ff24-430b-bd9b-af1aed4d28e9',
|
|
6
|
+
tenantId: '60e4e4d2-5765-40c2-9075-33a1a2aed068',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function makeContainer(scheduler: unknown) {
|
|
10
|
+
return {
|
|
11
|
+
resolve: <T = unknown>(_name: string) => scheduler as T,
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('ensureTerminalPollSchedule (create-only)', () => {
|
|
16
|
+
it('does not register when the schedule already exists (preserves UI edits)', async () => {
|
|
17
|
+
const register = vi.fn().mockResolvedValue(undefined)
|
|
18
|
+
const exists = vi.fn().mockResolvedValue(true)
|
|
19
|
+
|
|
20
|
+
await ensureTerminalPollSchedule(makeContainer({ register, exists }), scope)
|
|
21
|
+
|
|
22
|
+
expect(exists).toHaveBeenCalledTimes(1)
|
|
23
|
+
expect(register).not.toHaveBeenCalled()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('registers with module defaults when the schedule is missing', async () => {
|
|
27
|
+
const register = vi.fn().mockResolvedValue(undefined)
|
|
28
|
+
const exists = vi.fn().mockResolvedValue(false)
|
|
29
|
+
|
|
30
|
+
await ensureTerminalPollSchedule(makeContainer({ register, exists }), scope)
|
|
31
|
+
|
|
32
|
+
expect(register).toHaveBeenCalledTimes(1)
|
|
33
|
+
const reg = register.mock.calls[0][0] as Record<string, unknown>
|
|
34
|
+
// Same id is passed to exists() and register() so the guard is reliable.
|
|
35
|
+
expect(reg.id).toBe(exists.mock.calls[0][0])
|
|
36
|
+
expect(reg.scheduleType).toBe('cron')
|
|
37
|
+
expect(reg.scheduleValue).toBe('0 */4 * * *')
|
|
38
|
+
expect(reg.targetCommand).toBe('terminal_tracking.tracking.poll_all')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('falls back to register when the existence check throws', async () => {
|
|
42
|
+
const register = vi.fn().mockResolvedValue(undefined)
|
|
43
|
+
const exists = vi.fn().mockRejectedValue(new Error('boom'))
|
|
44
|
+
|
|
45
|
+
await ensureTerminalPollSchedule(makeContainer({ register, exists }), scope)
|
|
46
|
+
|
|
47
|
+
expect(register).toHaveBeenCalledTimes(1)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('registers when the scheduler has no exists() method (older scheduler)', async () => {
|
|
51
|
+
const register = vi.fn().mockResolvedValue(undefined)
|
|
52
|
+
|
|
53
|
+
await ensureTerminalPollSchedule(makeContainer({ register }), scope)
|
|
54
|
+
|
|
55
|
+
expect(register).toHaveBeenCalledTimes(1)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('no-ops when the scheduler module is not installed', async () => {
|
|
59
|
+
const container = {
|
|
60
|
+
resolve: () => {
|
|
61
|
+
throw new Error('not registered')
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await expect(ensureTerminalPollSchedule(container, scope)).resolves.toBeUndefined()
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
normalizeHoldCode,
|
|
4
|
+
getHoldInfo,
|
|
5
|
+
resolveHolds,
|
|
6
|
+
highestHoldSeverity,
|
|
7
|
+
} from '../holds'
|
|
8
|
+
|
|
9
|
+
describe('normalizeHoldCode', () => {
|
|
10
|
+
it('strips the leading "!" and upper-cases', () => {
|
|
11
|
+
expect(normalizeHoldCode('!dgd hold')).toBe('DGD HOLD')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('treats underscore- and space-delimited codes identically', () => {
|
|
15
|
+
expect(normalizeHoldCode('!TECHNICAL_FULL_CONTAINER')).toBe('TECHNICAL FULL CONTAINER')
|
|
16
|
+
expect(normalizeHoldCode('TECHNICAL FULL CONTAINER')).toBe('TECHNICAL FULL CONTAINER')
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('getHoldInfo', () => {
|
|
21
|
+
it('resolves a known critical hold with its explanation', () => {
|
|
22
|
+
const info = getHoldInfo('!CUSTOMS EXPORT HOLD')
|
|
23
|
+
expect(info.unknown).toBe(false)
|
|
24
|
+
expect(info.severity).toBe('critical')
|
|
25
|
+
expect(info.blocksMovement).toBe(true)
|
|
26
|
+
expect(info.category).toBe('export')
|
|
27
|
+
expect(info.rawCode).toBe('!CUSTOMS EXPORT HOLD')
|
|
28
|
+
expect(info.resolution).toMatch(/customs officer/i)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('classifies OCR technical holds as non-blocking info', () => {
|
|
32
|
+
const info = getHoldInfo('!TECHNICAL_FULL_CONTAINER')
|
|
33
|
+
expect(info.severity).toBe('info')
|
|
34
|
+
expect(info.blocksMovement).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('falls back to a visible warning for unknown codes and shows the raw hold', () => {
|
|
38
|
+
const info = getHoldInfo('!SOME_NEW_HOLD')
|
|
39
|
+
expect(info.unknown).toBe(true)
|
|
40
|
+
expect(info.severity).toBe('warning')
|
|
41
|
+
expect(info.i18nKey).toBe('unknown')
|
|
42
|
+
// Label echoes the raw hold (minus the `!`), not a normalized form.
|
|
43
|
+
expect(info.label).toBe('SOME_NEW_HOLD')
|
|
44
|
+
expect(info.rawCode).toBe('!SOME_NEW_HOLD')
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('resolveHolds', () => {
|
|
49
|
+
it('sorts most-urgent first and drops empties', () => {
|
|
50
|
+
const holds = resolveHolds([
|
|
51
|
+
'!TECHNICAL_FULL_CONTAINER',
|
|
52
|
+
'',
|
|
53
|
+
'!CUSTOMS EXPORT PERMISSION',
|
|
54
|
+
'!DGD HOLD',
|
|
55
|
+
])
|
|
56
|
+
expect(holds.map((h) => h.severity)).toEqual(['critical', 'warning', 'info'])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('returns an empty array for null/empty input', () => {
|
|
60
|
+
expect(resolveHolds(null)).toEqual([])
|
|
61
|
+
expect(resolveHolds([])).toEqual([])
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('highestHoldSeverity', () => {
|
|
66
|
+
it('returns the top severity', () => {
|
|
67
|
+
expect(highestHoldSeverity(['!TECHNICAL_FULL_CONTAINER', '!DGD HOLD'])).toBe('critical')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('returns null when there are no holds', () => {
|
|
71
|
+
expect(highestHoldSeverity([])).toBeNull()
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
parseTerminalStops,
|
|
4
|
+
isRoadStopped,
|
|
5
|
+
activeStopModes,
|
|
6
|
+
stopsChanged,
|
|
7
|
+
NO_STOPS,
|
|
8
|
+
} from '../stops'
|
|
9
|
+
|
|
10
|
+
describe('parseTerminalStops', () => {
|
|
11
|
+
it('returns all-null when no STOP columns are present (pre-update payload)', () => {
|
|
12
|
+
expect(parseTerminalStops({ 'Unit Nbr': 'ABCU1234567', Category: 'Import' })).toEqual(NO_STOPS)
|
|
13
|
+
expect(parseTerminalStops(null)).toEqual(NO_STOPS)
|
|
14
|
+
expect(parseTerminalStops(undefined)).toEqual(NO_STOPS)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('reads the documented Stop-Vsl / Stop-Road / Stop-Rail headers', () => {
|
|
18
|
+
expect(
|
|
19
|
+
parseTerminalStops({ 'Stop-Vsl': 'Y', 'Stop-Road': 'Y', 'Stop-Rail': 'Y' }),
|
|
20
|
+
).toEqual({ vsl: true, road: true, rail: true })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('distinguishes a present-but-empty column (false) from an absent one (null)', () => {
|
|
24
|
+
// Stop-Road present with no value -> reported, not stopped; Stop-Vsl absent -> unknown.
|
|
25
|
+
expect(parseTerminalStops({ 'Stop-Road': null, 'Stop-Rail': '' })).toEqual({
|
|
26
|
+
vsl: null,
|
|
27
|
+
road: false,
|
|
28
|
+
rail: false,
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('matches headers tolerant of casing and punctuation', () => {
|
|
33
|
+
expect(parseTerminalStops({ 'STOP VSL': 'Y' }).vsl).toBe(true)
|
|
34
|
+
expect(parseTerminalStops({ StopRoad: '1' }).road).toBe(true)
|
|
35
|
+
expect(parseTerminalStops({ 'stop_rail': 'true' }).rail).toBe(true)
|
|
36
|
+
expect(parseTerminalStops({ 'Stop-Vessel': 'Y' }).vsl).toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('treats explicit falsy tokens as no-stop', () => {
|
|
40
|
+
expect(parseTerminalStops({ 'Stop-Road': 'N' }).road).toBe(false)
|
|
41
|
+
expect(parseTerminalStops({ 'Stop-Road': 'No' }).road).toBe(false)
|
|
42
|
+
expect(parseTerminalStops({ 'Stop-Road': '0' }).road).toBe(false)
|
|
43
|
+
expect(parseTerminalStops({ 'Stop-Road': 'false' }).road).toBe(false)
|
|
44
|
+
expect(parseTerminalStops({ 'Stop-Road': '-' }).road).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('treats any other non-empty value as an active stop (e.g. a timestamp/reason)', () => {
|
|
48
|
+
expect(parseTerminalStops({ 'Stop-Vsl': '2026-08-04 10:00' }).vsl).toBe(true)
|
|
49
|
+
expect(parseTerminalStops({ 'Stop-Rail': 'CUSTOMS' }).rail).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('isRoadStopped / activeStopModes', () => {
|
|
54
|
+
it('isRoadStopped is true only on an explicit road stop', () => {
|
|
55
|
+
expect(isRoadStopped({ vsl: null, road: true, rail: null })).toBe(true)
|
|
56
|
+
expect(isRoadStopped({ vsl: null, road: false, rail: null })).toBe(false)
|
|
57
|
+
expect(isRoadStopped({ vsl: null, road: null, rail: null })).toBe(false)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('lists active modes in vsl -> road -> rail order', () => {
|
|
61
|
+
expect(activeStopModes({ vsl: true, road: false, rail: true })).toEqual(['vsl', 'rail'])
|
|
62
|
+
expect(activeStopModes(NO_STOPS)).toEqual([])
|
|
63
|
+
expect(activeStopModes({ vsl: true, road: true, rail: true })).toEqual(['vsl', 'road', 'rail'])
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('stopsChanged', () => {
|
|
68
|
+
it('detects an active-stop appearing or clearing on any mode', () => {
|
|
69
|
+
expect(stopsChanged(NO_STOPS, { vsl: null, road: true, rail: null })).toBe(true)
|
|
70
|
+
expect(stopsChanged({ vsl: null, road: false, rail: null }, { vsl: null, road: true, rail: null })).toBe(true)
|
|
71
|
+
expect(stopsChanged({ vsl: true, road: null, rail: null }, { vsl: false, road: null, rail: null })).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('treats null (unreported) and false (not stopped) as equivalent — no spurious change', () => {
|
|
75
|
+
// null → false is NOT a change: neither is an active stop.
|
|
76
|
+
expect(stopsChanged({ vsl: null, road: null, rail: null }, { vsl: false, road: false, rail: false })).toBe(false)
|
|
77
|
+
expect(stopsChanged(NO_STOPS, { vsl: null, road: null, rail: null })).toBe(false)
|
|
78
|
+
expect(stopsChanged({ vsl: true, road: false, rail: null }, { vsl: true, road: null, rail: false })).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { generateTotp, base32Decode, TOTP_PERIOD_SECONDS } from '../totp'
|
|
3
|
+
|
|
4
|
+
// RFC 6238 Appendix B publishes 8-digit reference codes for the SHA1 seed
|
|
5
|
+
// ASCII "12345678901234567890". Our generator emits 6 digits, i.e. the last 6
|
|
6
|
+
// of each published value. base32("12345678901234567890") = the seed below.
|
|
7
|
+
const RFC_SEED = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'
|
|
8
|
+
|
|
9
|
+
describe('generateTotp (RFC 6238 parity)', () => {
|
|
10
|
+
it('matches the published 6-digit truncations at fixed instants', () => {
|
|
11
|
+
// T=59s → 94287082 ; T=1234567890s → 89005924
|
|
12
|
+
expect(generateTotp(RFC_SEED, 59_000)).toBe('287082')
|
|
13
|
+
expect(generateTotp(RFC_SEED, 1_234_567_890_000)).toBe('005924')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('always returns a zero-padded 6-digit string', () => {
|
|
17
|
+
for (const t of [0, 30_000, 1_000_000_000_000, Date.now()]) {
|
|
18
|
+
expect(generateTotp(RFC_SEED, t)).toMatch(/^\d{6}$/)
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('rotates once per 30s period and is stable within one period', () => {
|
|
23
|
+
const base = 1_700_000_000_000 // arbitrary period boundary-ish
|
|
24
|
+
const inSamePeriod = generateTotp(RFC_SEED, base)
|
|
25
|
+
expect(generateTotp(RFC_SEED, base + 1_000)).toBe(inSamePeriod) // same window
|
|
26
|
+
const next = generateTotp(RFC_SEED, base + TOTP_PERIOD_SECONDS * 1_000)
|
|
27
|
+
expect(next).not.toBe(inSamePeriod) // next window differs
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('decodes base32 case-insensitively and ignoring whitespace', () => {
|
|
31
|
+
expect(base32Decode('jbswy3dpehpk3pxp')).toEqual(base32Decode('JBSWY3DPEHPK3PXP'))
|
|
32
|
+
expect(base32Decode('JBSW Y3DP EHPK 3PXP')).toEqual(base32Decode('JBSWY3DPEHPK3PXP'))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('rejects an invalid base32 secret', () => {
|
|
36
|
+
expect(() => generateTotp('not-base32!!')).toThrow(/base32/)
|
|
37
|
+
})
|
|
38
|
+
})
|