CTS-IL — Technical Reference
Table of Contents
- Architecture overview
- Module layout
- Kafka event bus
- Camel route inventory
- REST endpoints
- Guarantee lifecycle
- Gate-out decision flow (OPUS)
- Security
- Rate limiting
- Durable state — the poll roster
- Configuration reference
- Observability
1. Architecture overview
┌───────────────────────────────────────────────────────────────────┐
│ CTS-IL (port 8090) │
│ │
│ ┌──────────────┐ ┌────────────────┐ ┌──────────────────────┐ │
│ │ REST layer │ │ Camel routes │ │ Durable state │ │
│ │ (JAX-RS) │──│ (integration) │ │ (Redis) │ │
│ └──────────────┘ └───────┬────────┘ │ ActiveGuaranteeReg. │ │
│ │ └──────────────────────┘ │
└────────────────────────────┼──────────────────────────────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌─────▼──────┐ ┌───────▼──────┐ ┌───────▼──────┐
│ Kafka │ │ Maersk API │ │ Evergreen API│
│ (CTS bus) │ │ (REST/OAuth)│ │ (REST/OAuth)│
└────────────┘ └──────────────┘ └──────────────┘
│
┌─────▼──────┐
│ CTS │ (Container Tracking System — not in this repo)
└────────────┘
OPUS SmartLink ─────── POST /opus/events ──────────────▶ IL
SL agents ─────── POST /sl/guarantee/{id}/... ────▶ IL
The IL makes no decisions and holds no decision cache. Its only persistent state is the poll roster in Redis (see §10); all other durable state lives in CTS or in the Kafka log.
2. Module layout
cts-il/
├── pom.xml
└── src/
└── main/
├── java/com/capitalpay/cgmp/il/
│ ├── CgmpIlApplication.java # Quarkus entry point
│ ├── config/
│ │ ├── KafkaTopics.java # Topic name constants + header contract
│ │ ├── RateLimitProperties.java # Rate-limit config records
│ │ ├── MaerskProperties.java
│ │ ├── EvergreenProperties.java
│ │ └── OpusProperties.java
│ ├── model/
│ │ ├── kafka/ # Kafka message POJOs (Java records)
│ │ ├── maersk/ # Maersk API model objects
│ │ └── evergreen/ # Evergreen API model objects
│ ├── processor/
│ │ ├── ActiveGuaranteeRegistry.java # Per-carrier active container tracking
│ │ ├── maersk/
│ │ │ ├── MaerskGateEventProcessor.java
│ │ │ ├── MaerskHmacVerifier.java
│ │ │ └── MaerskDndProcessor.java
│ │ ├── evergreen/
│ │ │ ├── EvergreenGateEventProcessor.java
│ │ │ ├── EvergreenHmacVerifier.java
│ │ │ └── EvergreenDndProcessor.java
│ │ └── opus/
│ │ └── OpusHmacVerifier.java # verifies inbound webhook HMAC
│ ├── rest/
│ │ ├── MaerskWebhookResource.java
│ │ ├── EvergreenWebhookResource.java
│ │ ├── OpusEventResource.java
│ │ ├── SlConfirmationResource.java
│ │ └── TosRateLimiter.java
│ ├── routes/
│ │ ├── maersk/
│ │ │ ├── MaerskWebhookReceiverRoute.java
│ │ │ ├── MaerskEventPollerRoute.java
│ │ │ ├── MaerskDndFetcherRoute.java
│ │ │ ├── MaerskCertPusherRoute.java
│ │ │ └── MaerskSubscriptionManagerRoute.java (implied)
│ │ ├── evergreen/
│ │ │ ├── EvergreenWebhookReceiverRoute.java
│ │ │ ├── EvergreenEventPollerRoute.java
│ │ │ ├── EvergreenDndFetcherRoute.java
│ │ │ ├── EvergreenCertPusherRoute.java
│ │ │ └── EvergreenSubscriptionManagerRoute.java
│ │ ├── opus/
│ │ │ └── OpusEventReceiverRoute.java
│ │ └── sl/
│ │ └── SlReceiverRoute.java
│ └── security/
│ ├── MaerskOAuthClient.java
│ ├── EvergreenOAuthClient.java
│ └── OpusSmartLinkOAuthClient.java
└── resources/
└── application.properties
3. Kafka event bus
3.1 Topic listing
Inbound — guarantee service publishes, IL consumes
| Topic | Constant | Purpose |
|---|---|---|
cgmp.il.operation-requested |
IL_OPERATION_REQUESTED |
The guarantee service requests IL operations (SNAPSHOT, SUBSCRIBE, UNSUBSCRIBE, ROSTER_ADD, ROSTER_REMOVE) for one or more containers. See docs/contracts/il-operation-contract.md. |
The legacy
cgmp.guarantee.{issued,claim-decision,settled,closed}topics are no longer consumed by the IL at all — the lifecycle-topic consumers were removed in PBI-047. The IL is driven exclusively bycgmp.il.operation-requested. The guarantee service may still publish those topics for its own purposes; they're simply not part of the IL's contract anymore.
Outbound — IL publishes
| Topic | Constant | Purpose |
|---|---|---|
cgmp.il.operation-completed |
IL_OPERATION_COMPLETED |
One completion event per (requestId, containerNumber) (plus batch-level rejections) |
cgmp.gate-events |
GATE_EVENTS |
Normalised container gate and tracking events from all carriers and OPUS |
cgmp.dnd-charges |
DND_CHARGES |
D&D charge records from all carriers |
cgmp.sl.return-confirmation |
SL_RETURN_CONFIRMATION |
SL agent confirmed container returned to depot |
cgmp.sl.receipt-confirmed |
SL_RECEIPT_CONFIRMED |
SL agent confirmed payment received |
| Topic | Constant | Purpose |
|---|---|---|
cgmp.gate-events |
GATE_EVENTS |
Normalised container gate and tracking events from all carriers and OPUS |
cgmp.dnd-charges |
DND_CHARGES |
D&D charge records from all carriers |
cgmp.sl.return-confirmation |
SL_RETURN_CONFIRMATION |
SL agent confirmed container returned to depot |
cgmp.sl.receipt-confirmed |
SL_RECEIPT_CONFIRMED |
SL agent confirmed payment received (triggers CLOSED in CTS) |
3.2 Topic contracts
These topics form the IL ↔ CTS integration surface. Each entry documents the message type, payload fields, what triggers the publish, and the expected consumer action. All timestamps are UTC Instant (ISO-8601). Carrier identity is always duplicated in both the cgmp.slCode Kafka header and a payload field so consumers can route either before or after deserialisation.
Outbound contracts (IL → CTS)
cgmp.gate-events — GateEventMessage
Published whenever the IL receives or polls a container equipment event from any carrier, or when OPUS reports a TOS-level gate event.
| Field | Type | Description |
|---|---|---|
source |
String |
Origin of the event: MAERSK_WEBHOOK, MAERSK_POLL, EVERGREEN_WEBHOOK, EVERGREEN_POLL, or OPUS |
equipmentEventTypeCode |
String |
DCSA-standard code: GTOT (gate out full) or GTIN (gate in empty) |
containerNumber |
String |
ISO 6346 container number |
billOfLadingNumber |
String |
B/L reference; may be null for OPUS-sourced events |
eventDateTime |
Instant |
When the physical gate event occurred at the terminal |
facilityCode |
String |
UN/LOCODE or terminal code where the event occurred |
carrierCode |
String |
SCAC: MAEU, EGLV, etc. Mirrors the cgmp.slCode header |
Triggers: Maersk or Evergreen webhook delivery; carrier poll cycle (fallback); OPUS VESSEL_DISCHARGE_COMPLETE, CONTAINER_GATE_OUT_CONFIRMED, or FREE_TIME_EXPIRY_WARNING events.
CTS action: Update the container tracking record for the guarantee. A GTOT event starts the demurrage clock; a GTIN event stops it. VESSEL_DISCHARGE_COMPLETE creates the initial tracking record. FREE_TIME_EXPIRY_WARNING surfaces an early discrepancy flag.
cgmp.dnd-charges — DndChargeMessage
Published whenever the IL fetches D&D charges from a carrier API. The same topic is used for all carriers; CTS uses the slCode field and cgmp.slCode header to select the correct claim record type and rate card.
| Field | Type | Description |
|---|---|---|
containerNumber |
String |
ISO 6346 container number |
guaranteeId |
String |
UUID of the guarantee in CTS |
slCode |
String |
SCAC of the carrier whose D&D API produced these charges |
fetchTrigger |
String |
What triggered this fetch: SNAPSHOT (operation-driven), SCHEDULED (24 h poll), MANUAL |
fetchedAt |
Instant |
When the IL retrieved the charges from the carrier API |
charges |
List<ChargeItem> |
One entry per charge line; see below |
Each ChargeItem contains: chargeType (DEMURRAGE / DETENTION / STORAGE), status (ACCRUING / INVOICED), freeDaysGranted, freeDaysUsed, dailyRate, currency, accruedAmount, invoiced, invoiceNumber.
Triggers: SNAPSHOT — operation-driven via cgmp.il.operation-requested. SCHEDULED — 24-hour timer poll for all rostered containers. MANUAL — admin-driven.
CTS action: Create or update a D&D claim record (MAERSK_DND or EVERGREEN_DND based on slCode). Run the CTS vs SL cross-check against the applicable rate card. Raise a discrepancy flag if amounts diverge beyond the configured threshold.
cgmp.sl.return-confirmation — ReturnConfirmationMessage
Published when an SL agent confirms the container has been physically returned to the depot, or when OPUS reports a CONTAINER_GATE_IN_RETURN event.
| Field | Type | Description |
|---|---|---|
guaranteeId |
String |
UUID of the guarantee in CTS |
containerNumber |
String |
ISO 6346 container number |
slCode |
String |
SCAC of the carrier that owns the guarantee |
returnedAt |
Instant |
Timestamp of the return confirmation |
depotCode |
String |
Terminal/depot where the container was returned |
confirmedByAgent |
String |
Identity of the SL agent who submitted the confirmation |
shippingLineId |
String |
UUID of the ShippingLine entity in CTS — used to verify the agent's authority before processing |
Triggers: POST /sl/guarantee/{id}/confirm-return (SL agent REST call) or OPUS CONTAINER_GATE_IN_RETURN event.
Downstream action (not the IL's concern): the guarantee service is the authoritative consumer. The IL doesn't model what state-machine transitions it triggers.
cgmp.sl.receipt-confirmed — ReceiptConfirmedMessage
Published when an SL agent records their payment receipt reference, confirming they have received the guarantee payout.
| Field | Type | Description |
|---|---|---|
guaranteeId |
String |
UUID of the guarantee in CTS |
containerNumber |
String |
ISO 6346 container number |
slCode |
String |
SCAC of the carrier that owns the guarantee |
slReceiptRef |
String |
The SL's internal payment receipt reference |
confirmedAt |
Instant |
Timestamp of the confirmation |
confirmedBy |
String |
UUID of the SL agent user who submitted the receipt (from authenticated identity) |
Triggers: POST /sl/guarantee/{id}/confirm-receipt (SL agent REST call).
CTS action: Verify the confirming agent belongs to the guarantee's SL account. Transition guarantee to CLOSED. Issue the teardown operations to the IL on cgmp.il.operation-requested (UNSUBSCRIBE + ROSTER_REMOVE).
Inbound contract (guarantee service → IL)
The IL's sole inbound command channel is cgmp.il.operation-requested
(IlOperationRequestedMessage). The guarantee service requests operations —
SNAPSHOT, SUBSCRIBE, UNSUBSCRIBE, ROSTER_ADD, ROSTER_REMOVE — for one
or more containers; the IL executes them and reports back on
cgmp.il.operation-completed. The full schema, per-operation field
requirements, fan-out/rate-limit behaviour, and error codes live in
docs/contracts/il-operation-contract.md.
The legacy cgmp.guarantee.{issued,claim-decision,settled,closed} lifecycle
topics and their GuaranteeIssuedMessage / GuaranteeClosedMessage payloads
were removed from the IL in PBI-047. The IL no longer consumes them; the
guarantee service translates its own state transitions into the operations
above (see the state-machine mapping in the contract doc).
3.3 Kafka header contract
Every message on the CGMP bus, in both directions, must carry the following Kafka record headers:
| Header key | Constant | Format | Required |
|---|---|---|---|
cgmp.slCode |
HEADER_SL_CODE |
SCAC code: MAEU or EGLV |
Always |
cgmp.containerNo |
HEADER_CONTAINER_NO |
ISO 6346, e.g. MSCU1234567 |
On container-scoped messages |
cgmp.guaranteeId |
HEADER_GUARANTEE_ID |
UUID | Where guarantee is known |
The cgmp.slCode header is the primary routing discriminator. IL routes branch on this header before deserialising the message body. This avoids unnecessary JSON parsing on the hot path and ensures routing is deterministic even as body schemas evolve.
CTS responsibility: CTS must set cgmp.slCode on every message it publishes to inbound IL topics.
3.3 Partition key convention
All Kafka partition keys use the format slCode:containerNumber (e.g., MAEU:MSCU1234567). This makes the shipping line visible in the key itself — consumers and monitoring tools can identify the carrier without deserialising the body. Per-container ordering is preserved because slCode is stable for the lifetime of a guarantee.
KafkaTopics.partitionKey(slCode, containerNumber) is the canonical builder.
4. Camel route inventory
4.1 Maersk routes
| Route ID | Entry point | Description |
|---|---|---|
maersk-webhook-receiver |
direct:maersk-webhook-event |
Verifies HMAC, normalises event to GateEventMessage, publishes to cgmp.gate-events |
maersk-event-poller |
Timer (5 min) | Walks a roster slice; per the track batch mode either fetches per container (maersk-fetch-track-events) or groups the slice by B/L and fetches per B/L (maersk-fetch-track-events-by-bl) |
maersk-fetch-track-events |
direct:maersk-fetch-track-events |
Per-container T&T fetch (equipmentReference); publishes to cgmp.gate-events; rate-limited to 150 req/min |
maersk-fetch-track-events-by-bl |
direct:maersk-fetch-track-events-by-bl |
GROUP_BY_BL T&T fetch (transportDocumentReference — all containers on a B/L in one call); same 150 req/min bucket |
maersk-dnd-scheduled-poller |
Timer (24 h) | Groups the roster slice by (billOfLadingNumber, carrierCustomerCode) and fans out one maersk-dnd-fetcher call per group |
maersk-dnd-fetcher |
direct:maersk-fetch-dnd |
B/L-keyed D&D fetch (/import/{chargeType}?billOfLadingNumber=, one call per DEM/DET); demuxes equipmentCharges[] to per-container cgmp.dnd-charges; 60 req/min. Maersk D&D has no per-container query, so this is always GROUP_BY_BL |
maersk-subscription-create |
direct:maersk-subscribe |
Registers a Maersk webhook subscription (SUBSCRIBE op) |
maersk-subscription-delete |
direct:maersk-unsubscribe |
Deletes the Maersk webhook subscription (UNSUBSCRIBE op) |
4.2 Evergreen routes
| Route ID | Entry point | Description |
|---|---|---|
evergreen-webhook-receiver |
direct:evergreen-webhook-event |
Verifies HMAC, translates Evergreen event codes to DCSA, publishes to cgmp.gate-events |
evergreen-event-poller |
Timer (5 min) | Iterates the roster and delegates per container to evergreen-fetch-track-events |
evergreen-fetch-track-events |
direct:evergreen-fetch-track-events |
Per-container T&T fetch (poller + SNAPSHOT op); publishes to cgmp.gate-events; rate-limited to 100 req/min |
evergreen-dnd-scheduled-poller |
Timer (24 h) | Fans out to evergreen-dnd-fetcher |
evergreen-dnd-fetcher |
direct:evergreen-fetch-dnd |
On-demand D&D fetch; publishes to cgmp.dnd-charges; rate-limited to 60 req/min |
evergreen-subscription-create |
direct:evergreen-subscribe |
Registers an Evergreen webhook subscription (SUBSCRIBE op) |
evergreen-subscription-delete |
direct:evergreen-unsubscribe |
Deletes the Evergreen webhook subscription (UNSUBSCRIBE op) |
4.3 OPUS / SmartLink routes
| Route ID | Entry point | Description |
|---|---|---|
opus-event-receiver |
direct:opus-tos-event |
Dispatches by eventType (see §7); auth is performed at the resource layer before this route is invoked |
4.4 SL Receiver routes (SL agent → IL)
| Route ID | Entry point | Description |
|---|---|---|
sl-receiver-return |
direct:sl-return-confirmation |
Publishes ReturnConfirmationMessage to cgmp.sl.return-confirmation |
sl-receiver-receipt |
direct:sl-receipt-confirmed |
Publishes ReceiptConfirmedMessage to cgmp.sl.receipt-confirmed; triggers CTS CLOSED |
5. REST endpoints
All endpoints listen on port 8090. Webhook paths are called by external carrier systems; SL confirmation paths are called by authenticated SL agents.
Webhook receivers
POST /webhooks/maersk/events
Receives Maersk equipment event notifications.
- Auth: HMAC-SHA256 in
X-Maersk-Signatureheader (verified inMaerskHmacVerifier) - Body: Maersk
EquipmentEventJSON (MaerskEquipmentEvent) - Response:
202 Accepted - On failure:
400 Bad Requestif signature invalid
POST /webhooks/evergreen/events
Equivalent endpoint for Evergreen.
- Auth: HMAC signature in
X-Evergreen-Signature(verified inEvergreenHmacVerifier) - Body: Evergreen
EquipmentEventJSON (EvergreenEquipmentEvent)
POST /tos/events
Receives TOS events from OPUS Terminal via SmartLink.
- Auth: Bearer token in
Authorization(OpusBearerTokenVerifier, → 401) and HMAC-SHA256 body signature inX-OPUS-Signature(OpusHmacVerifier, → 400) - Rate limit: 200 req/min (
TosRateLimiter;HTTP 429beyond cap) - Body: Map with
eventTypediscriminator - Handling: gate events relayed to
cgmp.gate-events, all other types discarded — see §7. The IL returns no decision; responses are202 Accepted.
SL agent confirmations
POST /sl/guarantee/{id}/confirm-return
SL agent confirms the container has physically returned to the depot.
- Auth: Bearer token (SL agent identity)
- Body:
ReturnConfirmationMessagecontainerNumber,slCode,returnedAt(ISO 8601),depotCode,confirmedByAgent
- Effect:
returnedAtbecomes the authoritative D&D boundary. CTS freezes the D&D clock at this timestamp.
POST /sl/guarantee/{id}/confirm-receipt
SL agent confirms all payments were received and provides their accounts-receivable reference. Triggers CTS CLOSED.
- Auth: Bearer token (SL agent identity)
- Body:
ReceiptConfirmedMessagecontainerNumber,slCode,slReceiptRef,confirmedBy,confirmedAt
- Effect: Locks the guarantee. No further claims, payments, or modifications are possible.
6. Guarantee lifecycle
The state machine lives in CTS. IL participates as an event gateway:
┌──────────────┐
│ PENDING │ (CTS internal)
└──────┬───────┘
│ SNAPSHOT + SUBSCRIBE + ROSTER_ADD ops issued to IL
┌──────▼───────┐
│ ARMED │ container on the IL roster, webhook subscribed
└──────┬───────┘
│ container gate-out events from IL
┌──────▼───────┐
│ ACTIVE │ (CTS internal — demurrage clock running)
└──────┬───────┘
│ claim-decision (CTS-internal; SL notified out-of-band)
┌──────▼───────┐
│ SETTLED │ (SL agent verifies wallet via their own channel)
└──────┬───────┘
│ /confirm-return (DISARM)
┌──────▼───────┐
│ DISARMED │ D&D clock frozen, final charges computed
└──────┬───────┘
│ /confirm-receipt → UNSUBSCRIBE + ROSTER_REMOVE ops issued to IL
┌──────▼───────┐
│ CLOSED │ webhook subscription deleted, container off the roster
└──────────────┘
Key IL responsibilities per transition:
| Transition | IL action |
|---|---|
SNAPSHOT / SUBSCRIBE / ROSTER_ADD ops received |
Fetch a D&D/T&T baseline, register a carrier webhook subscription, and add the container to ActiveGuaranteeRegistry — one operation each, issued by the guarantee service on cgmp.il.operation-requested |
| Gate/tracking events received | Normalise to GateEventMessage; publish to cgmp.gate-events |
| Claim decision / settlement (CTS-internal) | No IL involvement — SL communication is out-of-band; the IL is never told about these transitions |
POST /sl/guarantee/{id}/confirm-return |
Publish ReturnConfirmationMessage → cgmp.sl.return-confirmation |
POST /sl/guarantee/{id}/confirm-receipt |
Publish ReceiptConfirmedMessage → cgmp.sl.receipt-confirmed |
UNSUBSCRIBE / ROSTER_REMOVE ops received |
Delete the carrier webhook subscription and remove the container from ActiveGuaranteeRegistry — issued by the guarantee service on cgmp.il.operation-requested |
7. OPUS gate-event relay
OPUS is treated like any shipping line under the ingress-only rule: it is an
inbound webhook event source only. There is no OPUS API the IL queries, and
the IL makes no gating decision — it does not answer truck-arrival requests
with ALLOW/HOLD. (The earlier synchronous CONTAINER_GATE_OUT_REQUEST →
ALLOW/HOLD machinery and its GuaranteeCache were removed; gating is the
terminal operator's concern, driven out-of-band by the guarantee state the
operator already holds.)
OPUS POSTs events to POST /tos/events. The resource authenticates each event
(HMAC-SHA256 over the raw body and a shared bearer token), enforces the
inbound rate cap, then hands the raw bytes to direct:opus-tos-event. The
opus-event-receiver route relays the two gate events the IL cares about and
discards everything else.
Relay path
OPUS → POST /tos/events
│
▼
OpusHmacVerifier + OpusBearerTokenVerifier (1) verify HMAC + bearer token
│
▼
OpusEventResource → direct:opus-tos-event (2) rate-limit, then dispatch
│
▼
opus-event-receiver .choice() on eventType
├─ CONTAINER_GATE_OUT_CONFIRMED ──▶ publish to cgmp.gate-events
├─ CONTAINER_GATE_IN_RETURN ──▶ publish to cgmp.gate-events
└─ otherwise ──▶ discard with warning log
Both relayed events are published to cgmp.gate-events with the Kafka key set
to the container number. CTS consumes cgmp.gate-events and applies its own
demurrage-clock semantics — the IL attaches no interpretation.
OPUS event types handled
eventType |
Action |
|---|---|
CONTAINER_GATE_OUT_CONFIRMED |
Publish to cgmp.gate-events (container left the terminal gate) |
CONTAINER_GATE_IN_RETURN |
Publish to cgmp.gate-events (container returned through the gate) |
| All other | Discarded with warning log |
8. Security
HMAC-SHA256 webhook verification
All inbound webhooks (Maersk, Evergreen, OPUS) are verified before any processing occurs. The verifier is the first processor in each route. An invalid signature throws IllegalArgumentException, which the route's onException handler traps, logs, and converts to HTTP 400.
| Verifier class | Header inspected | Secret config key |
|---|---|---|
MaerskHmacVerifier |
X-Maersk-Signature |
MAERSK_WEBHOOK_SECRET |
EvergreenHmacVerifier |
X-Evergreen-Signature |
EVERGREEN_WEBHOOK_SECRET |
OpusHmacVerifier |
OPUS-specific | OPUS_HMAC_SECRET |
OAuth 2.0 client credentials
Outbound calls to carrier APIs use tokens obtained via the OIDC client credentials flow. Quarkus manages token acquisition and refresh automatically.
| Named OIDC client | Target | Scopes |
|---|---|---|
maersk |
Maersk API | track:read, dnd:read, notifications:write |
evergreen |
Evergreen API | (carrier-defined) |
opus |
OPUS SmartLink | (carrier-defined) |
Token endpoints are configurable via environment variables (see §11).
SL agent authentication
SL agent REST calls (/sl/guarantee/...) require a valid Bearer token. The confirmedBy field in confirmation messages is populated from the authenticated identity at the REST layer — it is not trusted from the request body.
9. Rate limiting
IL enforces rate limits in both directions to comply with carrier API quotas and protect OPUS.
Outbound — carrier API calls
Rate limits are applied using Camel throttle() with a 60-second window.
| API | Operation | Default limit | Config property |
|---|---|---|---|
| Maersk | Track & Trace | 150 req/min | MAERSK_RATE_LIMIT_TRACK |
| Maersk | D&D | 60 req/min | MAERSK_RATE_LIMIT_DND |
| Maersk | Notifications | 30 req/min | MAERSK_RATE_LIMIT_NOTIFICATIONS |
| Evergreen | Track & Trace | 100 req/min | EVERGREEN_RATE_LIMIT_TRACK |
| Evergreen | D&D | 60 req/min | EVERGREEN_RATE_LIMIT_DND |
| Evergreen | Notifications | 30 req/min | EVERGREEN_RATE_LIMIT_NOTIFICATIONS |
Throttle behaviour: Camel throttle applies back-pressure — messages queue, they do not drop. Queued messages will be processed once the rate window clears.
Inbound — OPUS TOS events
OPUS inbound events are capped at 200 req/min (TosRateLimiter). Requests beyond the cap receive HTTP 429. SmartLink is expected to back off and retry (per CGMP×OPUS Integration Spec §8).
SL agent confirmation endpoints (/sl/guarantee/...) are not rate-limited because they are human-initiated, low-frequency events bounded by the number of active guarantees.
10. Durable state — the poll roster
The IL holds no decision-making cache. The only state it persists is the poll
roster: the set of containers each SL's pollers must cover. (The earlier
GuaranteeCache used for synchronous OPUS gate decisions was removed — see §7.)
ActiveGuaranteeRegistry
A Redis-backed roster, one hash per SL: cgmp:il:roster:{slCode}, with the
container number as the hash field and a small RosterEntry
(billOfLadingNumber, carrierCustomerCode) JSON as the value. It is read by
the polling routes (MaerskEventPollerRoute, EvergreenEventPollerRoute,
MaerskDndFetcherRoute, EvergreenDndFetcherRoute) to know which containers to
include in each poll cycle when webhooks are unavailable.
| Operation | Redis command | Driven by |
|---|---|---|
addContainer(slCode, containerNo, bl, custCode) |
HSET cgmp:il:roster:{slCode} |
ROSTER_ADD operation |
removeContainer(slCode, containerNo) |
HDEL (null slCode removes from both SL hashes) |
ROSTER_REMOVE operation |
nextSlice(slCode, api, countHint) |
HSCAN + persisted cursor (fail-soft: empty slice + WARN on Redis error) |
poller ticks |
getMaerskContainers() / getEvergreenContainers() |
HKEYS (fail-soft) |
whole-roster callers |
lookup(slCode, containerNo) |
HGET |
enrichment |
The roster is driven exclusively by ROSTER_ADD / ROSTER_REMOVE
operations on cgmp.il.operation-requested (the legacy cgmp.guarantee.*
@Incoming consumers were removed in PBI-047).
Bounded polling (PBI-055): the timer pollers do not re-read the whole
roster each tick — at production roster sizes (≈4,000 containers) a full sweep
can't drain within one period, so re-submitting it every tick would compound
the throttle backlog without bound. Instead each tick processes a bounded
slice via nextSlice, which walks the roster with HSCAN and persists a
per-{slCode}:{api} cursor in cgmp:il:sweep-cursor:{slCode}:{api} so
successive ticks cover the rest. Slice size = rate-limit × (period / 1 min),
sized to drain within the period; full-roster coverage latency is therefore
roster / rate (~27 min for Maersk T&T). A per-{slCode}:{api} non-overlap
guard (PollSweepGuard) additionally skips a tick whose predecessor is still
running. Webhooks (SUBSCRIBE) remain the primary real-time path; the poller
is reconciliation. See docs/pbi/PBI-055-cursored-reconciliation-sweep.md.
Durability: because the roster lives in persistent, replicated Redis (AOF
in dev via docker-compose; managed Redis in prod via REDIS_URL), it
survives IL restarts. A freshly restarted IL reads the existing roster from
Redis and resumes polling immediately — no replay or re-arming from the
guarantee service is required. See docs/pbi/PBI-054-durable-roster.md.
11. Configuration reference
All sensitive values are injected via environment variables. Non-sensitive defaults are defined in application.properties.
Core
| Property | Env var | Default | Description |
|---|---|---|---|
quarkus.http.port |
— | 8090 |
HTTP server port |
kafka.bootstrap.servers |
KAFKA_BOOTSTRAP_SERVERS |
localhost:9092 |
Kafka broker list |
Maersk
| Property | Env var | Default | Description |
|---|---|---|---|
maersk.api.base-url |
MAERSK_API_BASE_URL |
https://api.maersk.com |
API base URL |
maersk.api.consumer-key |
MAERSK_CONSUMER_KEY |
— | OAuth2 client ID |
maersk.api.consumer-secret |
MAERSK_CONSUMER_SECRET |
— | OAuth2 client secret |
maersk.api.oauth-token-url |
MAERSK_OAUTH_TOKEN_URL |
https://api.maersk.com/oauth2/access_token |
Token endpoint |
maersk.api.webhook.secret |
MAERSK_WEBHOOK_SECRET |
— | HMAC verification secret |
maersk.api.dnd.poll-interval-ms |
MAERSK_DND_POLL_INTERVAL_MS |
3600000 (1 h) |
D&D polling interval |
Evergreen
| Property | Env var | Default | Description |
|---|---|---|---|
evergreen.api.base-url |
EVERGREEN_API_BASE_URL |
https://api.evergreen-line.com |
API base URL |
evergreen.api.client-id |
EVERGREEN_CLIENT_ID |
— | OAuth2 client ID |
evergreen.api.client-secret |
EVERGREEN_CLIENT_SECRET |
— | OAuth2 client secret |
evergreen.api.oauth-token-url |
EVERGREEN_OAUTH_TOKEN_URL |
https://api.evergreen-line.com/oauth/token |
Token endpoint |
evergreen.api.webhook.secret |
EVERGREEN_WEBHOOK_SECRET |
— | HMAC verification secret |
evergreen.api.dnd.poll-interval-ms |
EVERGREEN_DND_POLL_INTERVAL_MS |
3600000 (1 h) |
D&D polling interval |
OPUS / SmartLink
| Property | Env var | Default | Description |
|---|---|---|---|
opus.smartlink.hmac-secret |
OPUS_HMAC_SECRET |
— | HMAC-SHA256 secret for inbound POST /tos/events |
opus.smartlink.inbound-bearer-token |
OPUS_INBOUND_BEARER_TOKEN |
— | Bearer token CGMP requires on inbound POST /tos/events (second factor) |
OPUS has no queryable API, so there is no base-URL / OAuth / outbound config — only the two secrets used to authenticate the inbound webhook.
Rate limits
| Property | Env var | Default | Description |
|---|---|---|---|
cgmp.rate-limit.maersk.track-api-per-min |
MAERSK_RATE_LIMIT_TRACK |
150 |
|
cgmp.rate-limit.maersk.dnd-api-per-min |
MAERSK_RATE_LIMIT_DND |
60 |
|
cgmp.rate-limit.maersk.notifications-api-per-min |
MAERSK_RATE_LIMIT_NOTIFICATIONS |
30 |
|
cgmp.rate-limit.evergreen.track-api-per-min |
EVERGREEN_RATE_LIMIT_TRACK |
100 |
|
cgmp.rate-limit.evergreen.dnd-api-per-min |
EVERGREEN_RATE_LIMIT_DND |
60 |
|
cgmp.rate-limit.evergreen.notifications-api-per-min |
EVERGREEN_RATE_LIMIT_NOTIFICATIONS |
30 |
|
cgmp.rate-limit.opus.inbound-events-per-min |
OPUS_RATE_LIMIT_INBOUND |
200 |
12. Observability
Health
GET /health — SmallRye Health composite endpoint.
Individual probes:
GET /health/live— livenessGET /health/ready— readiness (includes Kafka connectivity check)
Metrics
GET /metrics — Prometheus text format via Micrometer.
Key metrics to watch:
- Camel route exchange counts and failure rates (
camel_exchanges_*) - Kafka consumer lag (via JMX / Kafka consumer group lag tooling)
- JVM memory and GC (standard Micrometer JVM metrics)
- Custom rate-limit rejection counter (when
TosRateLimiterfires429)
Logging
Default log levels (application.properties):
| Logger | Level |
|---|---|
| Root | INFO |
com.capitalpay.cgmp.il |
DEBUG |
org.apache.camel |
INFO |
All route log statements include the container number and/or Kafka record key to allow correlation with CTS logs and carrier API audit trails.