📦 EaglesNest — Message Flow Diagrams

Visual guide to outbound (Receive) and inbound (Send) CargoWise XML flows.

Decision / Check
Rejected (error returned)
Success path
Background (after reply sent)
Delivery transport
Error notification email
flowchart TD A([" 📨 CargoWise sends XML\n POST /api/cargowisedata/receive "]):::start A --> B{{"🔐 Is the caller\nauthenticated?\nBearer or Basic"}}:::decision B -- "❌ No token\nor wrong token" --> C(["🚫 401 Unauthorised\nReason explained\nin response body"]):::fail B -- "✅ Valid token" --> D{{"📋 Are required headers\npresent?\nRecipientID + EDIClientName"}}:::decision D -- "❌ Missing\nheaders" --> E(["🚫 400 Bad Request"]):::fail D -- "✅ Headers OK" --> F{{"📂 Is this sender\nregistered?\ntblMessageConfig"}}:::decision F -- "❌ Unknown sender" --> G(["🚫 403 Forbidden\nSender not registered\nContact admin to add them"]):::fail G --> UE(["📧 Alert email sent\nto admin — throttled\n1 email per sender per hour"]):::notify F -- "✅ Found" --> H{{"🟢 Is the sender\nactive?\nStatus = true"}}:::decision H -- "❌ Disabled" --> I(["🚫 403 Forbidden\nThis sender has\nbeen turned off"]):::fail H -- "✅ Active" --> J{{"🔀 Is routing\nenabled?\ntblMessageFilter"}}:::decision J -- "⚠️ Routing OFF" --> K(["💾 Save file to disk\n✅ Return OK to CargoWise\n⏭️ No forwarding — disk only"]):::diskonly J -- "✅ Routing ON" --> L(["💾 Save file to disk\n✅ Return OK to CargoWise"]):::success L --> M(["⚙️ Background task starts\nA fresh database connection\nis created just for this"]):::bg M --> N{{"📋 Any active\ndelivery routes?\ntblRoutes status = true"}}:::decision N -- "❌ No routes\nconfigured" --> O(["📝 Log: no routes found\nFile stays on disk"]):::warn N -- "✅ Routes found" --> P(["🚀 Send to ALL routes\nat the same time"]):::bg P --> RETRY(["🔄 Each route: up to 3 attempts\nBack-off: 5 s then 10 s between tries\nConfig errors skip retries immediately"]):::bg RETRY --> Q(["📁 FTP / SFTP\nUpload to\nremote server"]):::transport RETRY --> R(["🌐 HTTP / HTTPS\nPOST XML to\nAPI endpoint"]):::transport RETRY --> S(["🔑 OUTBOUNDNEXT\nGet OAuth2 token first\nthen POST XML"]):::transport RETRY --> T(["🧼 SOAP / ENVELOP\nSOAP POST with\nSOAPAction header"]):::transport RETRY --> U(["🗂️ LOCAL\nCopy file to\nlocal folder"]):::transport Q & R & S & T & U --> V(["📊 Record delivery count\ntblMessageCount +1\nper successful route"]):::bg V --> W(["📝 Log results\nSuccess ✅ or Failure ❌\nper route"]):::bg W --> NA{{"❌ Any routes still\nfailed after 3 attempts?"}}:::decision NA -- "✅ All OK" --> NB(["✅ Done"]):::success NA -- "❌ Has failures" --> RF(["🗄️ Record each failed route\nto tblFailedMessage\nconfig ID · filename · error · datetime"]):::fail RF --> ALLQ{{"📦 Did ALL routes fail?\n(nothing delivered)"}}:::decision ALLQ -- "✅ Some routes\ndelivered OK" --> NC ALLQ -- "❌ Total failure" --> FMOVE(["📁 Copy file to FailedFolder\nfor review & reprocessing\nfailed_path saved to DB"]):::fail FMOVE --> NC NC{{"📋 Notification tracker\nexists in tblNotificationTracker?"}}:::decision NC -- "First time\n(no row yet)" --> ND(["📧 Send plain-English\nalert email now\n+ Create tracker row"]):::notify NC -- "Row found" --> NE{{"⏰ Throttle window\nelapsed?\nHourly / Daily / Minutes"}}:::decision NE -- "❌ Still within\nwindow" --> NF(["🔇 Suppress email\nAvoid flooding inbox"]):::warn NE -- "✅ Window elapsed" --> NG(["📧 Send plain-English\nalert email\n+ Update last-sent time"]):::notify classDef start fill:#1e293b,stroke:#6366f1,stroke-width:3px,color:#e2e8f0,font-weight:bold classDef decision fill:#1e293b,stroke:#6366f1,stroke-width:2px,color:#c4b5fd classDef fail fill:#450a0a,stroke:#f87171,stroke-width:2px,color:#fca5a5 classDef success fill:#052e16,stroke:#34d399,stroke-width:2px,color:#86efac classDef diskonly fill:#451a03,stroke:#fbbf24,stroke-width:2px,color:#fde68a classDef bg fill:#1c1917,stroke:#fbbf24,stroke-width:2px,color:#fde68a,stroke-dasharray:5 4 classDef transport fill:#0c1a35,stroke:#38bdf8,stroke-width:2px,color:#7dd3fc classDef warn fill:#1c1917,stroke:#94a3b8,stroke-width:1px,color:#94a3b8 classDef notify fill:#1a0a2e,stroke:#a78bfa,stroke-width:2px,color:#ddd6fe

📬 Delivery Route Types

FTP / FTPS

Uploads the file to a remote FTP server. Always uses encrypted FTPS (TLS) — plain FTP is never sent. Username + password login.

Port 21

SFTP

Uploads the file over a secure SSH connection. Different from FTPS — uses SSH protocol, not TLS. Username + password login.

Port 22

HTTP / HTTPS

Sends the XML file as a POST request to a web API endpoint. Supports Basic Auth (username + password) or a static bearer token.

REST API

OUTBOUNDNEXT

Like HTTP/HTTPS but uses OAuth 2.0 — first contacts a login server to get a temporary access token, then sends the file. Token is reused until it expires (auto-refreshed 60 seconds early).

OAuth 2.0

SOAP

Sends the XML as a SOAP 1.1 message to a web service. Adds a SOAPAction header that tells the server which operation to run. Supports Basic Auth, bearer token, or SOAPAction-only (no auth).

text/xml

ENVELOP

Same as SOAP but uses the newer SOAP 1.2 content type. For systems that require application/soap+xml instead of text/xml.

application/soap+xml

LOCAL

Copies the file to a folder on the same server (or a mapped network drive). No login needed — just a destination folder path.

File Copy

📁 Failed Folder

When every route fails after 3 attempts, the original XML file is copied here for review and manual reprocessing. The path is saved in tblFailedMessage.failed_path.

All routes failed

If only some routes fail, the file is not copied — it was already delivered by the successful routes.

🔄 Retry Policy

Every route is tried up to 3 times before being marked as failed. A short wait is added between attempts to give the destination time to recover.

Attempt 1 Wait 5 s → Attempt 2 Wait 10 s → Attempt 3

Configuration errors (e.g. unknown route type) skip retries immediately — they won't fix themselves.

🔐 SOAP / ENVELOP — Auth Selection Logic

Password set?Token set?SOAPAction set?What gets sent
✅ Yes❌ NoBasic Auth  (username:password base64)
✅ Yes✅ YesBearer Token  (Authorization: Bearer)
❌ No✅ YesSOAPAction header only  (no auth)
❌ No✅ Yes❌ NoBearer Token
❌ No❌ No❌ NoNo auth  (unauthenticated)

📧 Error Notification — How It Works

EaglesNest sends a plain-English alert email in two situations:
  • Delivery failure — when a file cannot be delivered to any route after 3 attempts.
  • Unknown sender — when the API receives a file from a sender that has no matching row in tblMessageConfig.

Throttle tracked in tblNotificationTracker. SMTP settings in appsettings.json → Notification.
occurrence_typeoccurrence_noMeaning
Hourly (default)1At most one email per hour
Hourly4At most one email every 4 hours
Daily1At most one email per day
Minutes30At most one email every 30 minutes
First failure everAlways sent immediately — tracker row is created on the spot
Endpoint: POST /api/cargowisedata/send
The caller authenticates with Basic Auth. The authenticated username is used as the partitionKey. EaglesNest derives the SenderID from the first 6 characters of the username (username[..6]), fetches a Bearer token from eAdapterNext.Token using the full username as the partitionKey, then forwards the XML to https://{SenderID}services.wisegrid.net/eAdaptorNext. If no Azure Table record exists for the username, an alert email is sent to the admin.
Decision / Check
Error returned to caller
Success path
External service call
Token cache hit
OAuth2 / Token acquisition
Admin notification email
flowchart TD A([" 📤 Caller POSTs XML\n POST /api/cargowisedata/send "]):::start A --> B{{"🔐 Basic Auth\nAuthenticated?"}}:::decision B -- "❌ Invalid / missing" --> C(["🚫 401 Unauthorised"]):::fail B -- "✅ Valid" --> BA(["👤 username = User.Identity.Name\n(Basic Auth credential)"]):::step BA --> BB{{"❌ username\n< 6 chars?"}}:::decision BB -- "Too short" --> BC(["🚫 400 Bad Request\nUsername must be ≥ 6 chars\n(first 6 used as SenderID)"]):::fail BB -- "✅ OK" --> D(["📖 Read raw XML body\nfrom HTTP request stream"]):::step D --> E{{"❌ Body read\nfailed?"}}:::decision E -- "Exception" --> F(["🚫 500 Internal Error\nFailed to read request body"]):::fail E -- "✅ OK" --> EE{{"❌ Body\nempty?"}}:::decision EE -- "Empty" --> EF(["🚫 400 Bad Request\nRequest body is empty"]):::fail EE -- "✅ Has content" --> G(["🔑 Derive SenderID = username[..6]\nBuild TargetUrl:\nhttps://{SenderID}services.wisegrid.net/eAdaptorNext"]):::step G --> K(["🎫 POST {TokenApiBaseUrl}/api/token\nbody: { partitionKey: '{username}' }\nContent-Type: application/json"]):::ext K --> L{{"📡 Token API\nreachable?"}}:::decision L -- "❌ Network error\nor timeout" --> M(["🚫 502 Bad Gateway\nCannot reach Token API\neAdapterNext.Token is down"]):::fail L -- "✅ Responded" --> N{{"✅ Token API\nsuccess=true?"}}:::decision N -- "❌ Config not found\n(no Azure Table row\nfor this username)" --> NA(["📧 Admin alert email sent\nThrottled: 1/hour per username"]):::notify NA --> O(["🚫 502 Bad Gateway\nNo Token config for this user.\nAdmin has been notified."]):::fail N -- "❌ Other error\n(Azure AD / token issue)" --> O2(["🚫 502 Bad Gateway\nToken API returned error"]):::fail N -- "✅ success=true" --> P{{"🗄️ Token source?"}}:::decision P -- "🟡 CACHED\n(still valid in\nAzure Table Storage)" --> Q(["⚡ Return cached token\nTokenSource: Cached\nno Azure AD call needed"]):::cache P -- "🆕 NEW\n(expired or\nfirst time)" --> R(["🔑 Token API called Azure AD\nOAuth2 client_credentials\nToken cached in Azure Table"]):::auth Q & R --> S(["📤 POST XML to CargoWise\nhttps://{SenderID}services.wisegrid.net/eAdaptorNext\nAuthorization: Bearer {token}\nContent-Type: application/xml"]):::ext S --> T{{"📡 CargoWise\nendpoint reachable?"}}:::decision T -- "❌ Network error\nor timeout" --> TNA(["📧 Admin alert email\nNotifySendFailureAsync\nThrottled: 1/hour per username"]):::notify TNA --> U(["🚫 502 Bad Gateway\nNetwork error to CargoWise\nEndpoint may be down"]):::fail T -- "✅ Response received" --> V{{"📊 CargoWise\nHTTP Status"}}:::decision V -- "✅ 2xx" --> W(["✅ 200 OK → success=true\nCargowiseSendResponse:\ntraceParent · sentAt · cwStatusCode\ncwResponse · tokenSource · targetUrl"]):::success V -- "⚠️ Non-2xx\n4xx / 5xx" --> VNA(["📧 Admin alert email\nNotifySendFailureAsync\nThrottled: 1/hour per username"]):::notify VNA --> X(["⚠️ 200 OK → success=false\nCargoWise rejected the XML\ncwStatusCode + cwResponse body returned"]):::warn classDef start fill:#1e293b,stroke:#6366f1,stroke-width:3px,color:#e2e8f0,font-weight:bold classDef decision fill:#1e293b,stroke:#6366f1,stroke-width:2px,color:#c4b5fd classDef fail fill:#450a0a,stroke:#f87171,stroke-width:2px,color:#fca5a5 classDef success fill:#052e16,stroke:#34d399,stroke-width:2px,color:#86efac classDef step fill:#0f1a2e,stroke:#38bdf8,stroke-width:2px,color:#7dd3fc classDef ext fill:#1c1005,stroke:#fb923c,stroke-width:2px,color:#fdba74 classDef cache fill:#1c1917,stroke:#fbbf24,stroke-width:2px,color:#fde68a classDef auth fill:#1a0a2e,stroke:#a78bfa,stroke-width:2px,color:#ddd6fe classDef warn fill:#451a03,stroke:#fbbf24,stroke-width:1px,color:#fde68a classDef notify fill:#0c1a0c,stroke:#4ade80,stroke-width:2px,color:#86efac

🎫 Token API Lookup — How It Works

eAdapterNext.Token (POST /api/token) manages OAuth2 tokens for outbound calls. The authenticated username is used as the partitionKey — it must match a row in Azure Table Storage containing the OAuth2 credentials (TenantId, ClientId, certificate PEM, private key PEM, Scope). If no row is found, a 502 is returned and an admin alert email is sent (throttled — 1/hour per username).

Tokens are cached in the same Azure Table row (AccessToken, ExpiresAtUtc). A 5-minute buffer is applied — the token is refreshed 5 minutes before it actually expires to avoid using a stale token.
ScenarioToken API ActionTokenSource in response
First request / token expiredOAuth2 client_credentials → Azure AD → store in tableNew
Within 5-min buffer before expiryOAuth2 refresh → Azure AD → update tableNew
Token still valid (> 5 min remaining)Return stored AccessToken directlyCached
Username not in Azure TableReturn success=false, message: "Config not found" — admin alert email sent❌ 502 from eAdapterNext.Auth
Azure AD refuses credentialsReturn success=false with Azure error detail❌ 502 from eAdapterNext.Auth

⚠️ Send Endpoint — Error Reference

HTTP Statussuccess fieldCauseFix
400falseUsername shorter than 6 characters, or empty request bodyEnsure Basic Auth username is ≥ 6 chars and XML body is present
401falseMissing or invalid Basic Auth credentialsSupply a valid Authorization: Basic ... header
500falseFailed to read the HTTP request bodyEnsure the XML body is sent correctly and the connection is stable
502falseNo Azure Table config for username (admin notified), Token API unreachable, or CargoWise network failureAdd the username row in eAdapterNext.Token Azure Table; check eAdapterSettings:TokenApiBaseUrl
200 + success=falsefalseCargoWise returned non-2xx (e.g. 400 or 500) — admin alert sentCheck cwResponse body for CargoWise error detail
200 + success=truetrueXML delivered successfully