Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

  • Kivra digital mailbox integration (planned for Phase 2)
  • e-Faktura/PEPPOL electronic invoicing (planned for Phase 2)
  • Customer self-service portal (planned for Phase 2)
  • Payment processing and reconciliation (future phase)
  • Invoice amendments and credit notes (future phase)
  • Advanced analytics and business intelligence (future phase)
  • SMS distribution via Wiraya (future phase)

Documentation Standards:

  • All technical diagrams created using Draw.io VSCode extension
  • Diagrams include source XML for version control
  • All findings documented in Confluence (link above)
  • Visual structures for flows and relationships
  • Searchable, structured format for all technical details

...

  1. Project Overview
  2. Business Requirements
  3. Functional Requirements
  4. Non-Functional Requirements
  5. Data Flow Diagrams
  6. API Specifications
  7. Error Handling & Validation
  8. Glossary
  9. Appendices

4. Non-Functional Requirements

5. Data Flow Diagrams

5.1 High-Level System Data Flow

┌─────────────────────────────────────────────────────────────────┐
│                     EG Flow Complete Data Flow                  │
└─────────────────────────────────────────────────────────────────┘

External System                    EG Flow System (Azure)
(Utility Billing)                                    
      │                                              
      │  1. Generate monthly                         
      │     invoice batch (XML)                      
      │                                              
      ├──────────────────────────────────────────>  
      │     POST /batches                    ┌──────────────┐
      │     (GASEL/XELLENT/ZYNERGY XML)      │  Core API    │
      │                                      │  Service     │
      │                                      │              │
      │                                      │ • Auth check │
      │                                      │ • Validate   │
      │                                      │ • Store blob │
      │                                      └──────┬───────┘
      │                                             │
      │  2. Return Batch ID                         │
      │<────────────────────────────────────────────┤
      │     {batchId, status: "uploaded"}           │
      │                                             │
      │  3. Start Processing                        │
      ├──────────────────────────────────────────>  │
      │     POST /batches/{id}/start                │
      │                                             │
      │                                      ┌──────▼───────┐
      │                                      │  Blob        │
      │                                      │  Storage     │
      │                                      │              │
      │                                      │ {org}-batches│
      │                                      │ /2025/11/21/ │
      │                                      └──────┬───────┘
      │                                             │
      │  4. 202 Accepted (queued)                   │
      │<────────────────────────────────────────────┤
      │                                             │
      │                                      ┌──────▼────────┐
      │                                      │  Storage      │
      │                                      │  Queues       │
      │                                      │               │
      │                                      │ • batch-upload│
      │                                      │ • batch-items │
      │                                      │ • email       │
      │                                      │ • postal-bulk │
      │                                      └───────┬───────┘
      │                                              │
      │                              ┌───────────────┼──────────────┐
      │                              │               │              │
      │                       ┌──────▼─────┐  ┌─────▼──────┐ ┌─────▼──────┐
      │                       │  Parser    │  │ Document   │ │  Email     │
      │                       │  Service   │  │ Generator  │ │  Service   │
      │                       │            │  │            │ │            │
      │                       │ • Detect   │  │ • Render   │ │ • SendGrid │
      │                       │ • Validate │  │ • PDF gen  │ │ • Retry    │
      │                       │ • Parse    │  │ • Store    │ │ • Fallback │
      │                       │ • Transform│  │            │ │            │
      │                       └──────┬─────┘  └─────┬──────┘ └─────┬──────┘
      │                              │              │              │
      │                       Creates 157    Creates PDFs    Sends emails
      │                       JSON files    & queues         │
      │                       (5000/32)     delivery         │
      │                              │              │              │
      │                              ▼              ▼              ▼
      │                       ┌──────────────────────────────────────┐
      │                       │         Blob Storage                 │
      │                       │                                      │
      │                       │ {org}-invoices-2025/11/21/           │
      │                       │ ├── {id}.json (canonical data)       │
      │                       │ ├── {id}.html (rendered)             │
      │                       │ └── {id}.pdf (final invoice)         │
      │                       └──────────────────────────────────────┘
      │                                              │
      │  5. Poll Status                              │
      │     GET /batches/{id}                        │
      ├───────────────────────────────────────────>  │
      │                                              │
      │  6. Status Response                          │
      │<─────────────────────────────────────────────┤
      │     {status: "processing",                   │
      │      processedItems: 3200/5000}              │
      │                                              │
      │  7. Status: Completed                        │
      │<─────────────────────────────────────────────┤
      │     {status: "completed",                    │
      │      successfulItems: 4950,                  │
      │      failedItems: 50}                        │
      │                                              │
      │                                      ┌───────▼──────┐
      │                                      │  Postal      │
      │                                      │  Service     │
      │                                      │              │
      │                                      │ • Collect    │
      │                                      │ • Create ZIP │
      │                                      │ • 21G SFTP   │
      │                                      └───────┬──────┘
      │                                              │
End Customer                                         │
      │                                              │
      │  8a. Receive Email                           │
      │<─────────────────────────────────────────────┤
      │      (PDF attachment)                        │
      │                                              │
      │  8b. Receive Postal                          │
      │<─────────────────────────────────────────────┤
      │      (via 21G print partner)                 │

...

5.2 Batch Upload Flow

┌──────────────────────────────────────────────────────────────┐
│                    Batch Upload Data Flow                    │
└──────────────────────────────────────────────────────────────┘

Client                 API Gateway           Core API              Blob Storage
  │                         │                    │                       │
  ├─ POST /batches ────────>│                    │                       │
  │  (XML file)             │                    │                       │
  │                         ├─ Authenticate ────>│                       │
  │                         │  (Entra ID)        │                       │
  │                         │<─ JWT Valid ───────┤                       │
  │                         │                    │                       │
  │                         ├─ Authorize ───────>│                       │
  │                         │  (Check role)      │                       │
  │                         │<─ Access OK ───────┤                       │
  │                         │                    │                       │
  │                         ├─ Upload File ─────>│                       │
  │                         │                    ├─ Generate UUID ──────>│
  │                         │                    │  (Batch ID)           │
  │                         │                    │                       │
  │                         │                    ├─ Store XML ──────────>│
  │                         │                    │  {org}-batches-2025/  │
  │                         │                    │  11/21/{id}/source.xml│
  │                         │                    │<─ Stored ─────────────┤
  │                         │                    │                       │
  │                         │                    ├─ Quick Detect ───────>│
  │                         │                    │  (Namespace peek)     │
  │                         │                    │<─ Format: GASEL ──────┤
  │                         │                    │                       │
  │                         │                    ├─ Create Metadata ────>│
  │                         │                    │  metadata.json        │
  │                         │                    │<─ Created ────────────┤
  │                         │                    │                       │
  │                         │<─ 201 Created ─────┤                       │
  │                         │  {batchId}         │                       │
  │<─ 201 Created ──────────┤                    │                       │
  │  {batchId, status}      │                    │                       │

...

5.3 Parser Service Detailed Flow (XML → JSON)

┌──────────────────────────────────────────────────────────────┐
│           Parser Service: XML → Canonical JSON Flow          │
└──────────────────────────────────────────────────────────────┘

Queue Message         Parser Service              Blob Storage
      │                     │                            │
      ├─ {batchId} ────────>│                            │
      │                     │                            │
      │                     ├─ Download XML ────────────>│
      │                     │  GET {org}-batches-2025/   │
      │                     │      11/21/{id}/source.xml │
      │                     │<─ XML Content ─────────────┤
      │                     │                            │
      │                     ├─ Parse Root Namespace      │
      │                     │  xmlns="urn:ediel:..."     │
      │                     │  → Detected: GASEL         │
      │                     │                            │
      │                     ├─ Load Schema ─────────────>│
      │                     │  GET {org}-data/schemas/   │
      │                     │      gasel-mapping.json    │
      │                     │<─ Mapping Config ──────────┤
      │                     │                            │
      │                     ├─ Load XSD ────────────────>│
      │                     │  GET {org}-data/schemas/   │
      │                     │      gasel-v1.0.xsd        │
      │                     │<─ XSD Content ─────────────┤
      │                     │                            │
      │                     ├─ Validate XML              │
      │                     │  Against XSD               │
      │                     │  Result: VALID             │
      │                     │                            │
      │                     ├─ Parse XML (XPath)         │
      │                     │  Extract Invoice[1]:       │
      │                     │   InvoiceNumber =          │
      │                     │     "2025-11-001"          │
      │                     │   CustomerName =           │
      │                     │     "Medeni Schröder"      │
      │                     │   TotalAmount = 749.28     │
      │                     │  ... (all fields)          │
      │                     │                            │
      │                     ├─ Transform to Canonical    │
      │                     │  {                         │
      │                     │    invoiceNumber,          │
      │                     │    invoiceDate,            │
      │                     │    customer: {...},        │
      │                     │    invoiceDetails: {...},  │
      │                     │    delivery: {...},        │
      │                     │    sourceMetadata: {       │
      │                     │      vendorCode: "GASEL"   │
      │                     │    }                       │
      │                     │  }                         │
      │                     │                            │
      │                     ├─ LOOP: All 5000 invoices   │
      │                     │                            │
      │                     ├─ Store JSON (5000×) ───>   │
      │                     │  PUT {org}-invoices-2025/  │
      │                     │      11/21/{inv-id}.json   │
      │                     │<─ Stored (5000×) ──────────┤
      │                     │                            │
      │                     ├─ Group into 32-batches     │
      │                     │  5000 ÷ 32 = 157 batches   │
      │                     │                            │
      │                     ├─ Enqueue 157 Messages      │
      │                     │  TO batch-items-queue      │
      │                     │  Each: 32 invoice IDs      │
      │                     │                            │
      │                     ├─ Update Batch Metadata ─>  │
      │                     │  PUT {org}-batches-2025/   │
      │                     │      11/21/{id}/           │
      │                     │      metadata.json         │
      │                     │  {                         │
      │                     │    totalItems: 5000,       │
      │                     │    vendorCode: "GASEL",    │
      │                     │    status: "processing"    │
      │                     │  }                         │
      │                     │<─ Updated ─────────────────┤
      │                     │                            │
      │<─ ACK (delete msg)──┤                            │
      │                     │                            │

...

5.4 Document Generator Flow (JSON → HTML → PDF)

┌──────────────────────────────────────────────────────────────┐
│        Document Generator: JSON → HTML → PDF Flow            │
└──────────────────────────────────────────────────────────────┘

Queue Message         Document Generator         Blob Storage
      │                     │                           │
      ├─ 32 Invoice IDs ───>│                           │
      │                     │                           │
      │                     ├─ Acquire Blob Lease ────> │
      │                     │  Lease: {batch}/locks/    │
      │                     │         {worker-id}.lock  │
      │                     │<─ Lease Acquired ─────────┤
      │                     │  (5 min duration)         │
      │                     │                           │
      │                     ├─ LOOP: 32 invoices        │
      │                     │                           │
      │                     ├─ Download JSON ──────────>│
      │                     │  GET {org}-invoices-2025/ │
      │                     │      11/21/{id}.json      │
      │                     │<─ Canonical Invoice ──────┤
      │                     │                           │
      │                     ├─ Load Org Config ────>    │
      │                     │  GET {org}-data/          │
      │                     │      organization.json    │
      │                     │<─ Branding, Settings ─────┤
      │                     │                           │
      │                     ├─ Determine Template       │
      │                     │  Category (invoice type)  │
      │                     │  → "invoice"              │
      │                     │                           │
      │                     ├─ Load Template ──────────>│
      │                     │  GET {org}-data/          │
      │                     │      templates/invoice/   │
      │                     │      active.html          │
      │                     │<─ Handlebars Template ────┤
      │                     │                           │
      │                     ├─ Compile Template         │
      │                     │  (cache 24h if not cached)│
      │                     │  Handlebars.Compile()     │
      │                     │                           │
      │                     ├─ Render HTML              │
      │                     │  Apply invoice data       │
      │                     │  + organization branding  │
      │                     │  Output: HTML string      │
      │                     │  Duration: ~1-2 seconds   │
      │                     │                           │
      │                     ├─ Generate PDF             │
      │                     │  Playwright.NewPage()     │
      │                     │  SetContentAsync(html)    │
      │                     │  PdfAsync(A4, margins)    │
      │                     │  Duration: ~3-5 seconds   │
      │                     │                           │
      │                     ├─ Store HTML ─────────────>│
      │                     │  PUT {org}-invoices-2025/ │
      │                     │      11/21/{id}.html      │
      │                     │<─ Stored ─────────────────┤
      │                     │                           │
      │                     ├─ Store PDF ──────────────>│
      │                     │  PUT {org}-invoices-2025/ │
      │                     │      11/21/{id}.pdf       │
      │                     │<─ Stored ─────────────────┤
      │                     │                           │
      │                     ├─ Update Invoice JSON ────>│
      │                     │  Add fileReferences,      │
      │                     │  templateInfo, timestamp  │
      │                     │<─ Updated ────────────────┤
      │                     │                           │
      │                     ├─ Determine Distribution   │
      │                     │  Has email? → email-queue │
      │                     │  Else → postal-bulk-queue │
      │                     │                           │
      │                     ├─ Enqueue Delivery         │
      │                     │  TO email-queue OR        │
      │                     │  TO postal-bulk-queue     │
      │                     │                           │
      │                     │  (END OF 32 ITEMS LOOP)   │
      │                     │                           │
      │                     ├─ Release Blob Lease ─────>│
      │                     │<─ Lease Released ─────────┤
      │                     │                           │
      │                     ├─ Update Batch Stats ─────>│
      │                     │  (ETag concurrency)       │
      │                     │  processedItems += 32     │
      │                     │<─ Updated ────────────────┤
      │                     │                           │
      │<─ ACK (delete msg)──┤                           │

...

5.5 Email Delivery Flow

┌───────────────────────────────────────────────────────────┐
│              Email Delivery Service Flow                  │
└───────────────────────────────────────────────────────────┘

email-queue      Email Service      SendGrid API      Blob Storage
      │                │                  │                │
      ├─ {invoiceId} ─>│                  │                │
      │                │                  │                │
      │                ├─ Download PDF ───────────────────>│
      │                │  GET {org}-invoices-2025/11/21/   │
      │                │      {invoice-id}.pdf             │
      │                │<─ PDF Bytes ──────────────────────┤
      │                │                  │                │
      │                ├─ Load Org Config ────────────────>│
      │                │  GET {org}-data/organization.json │
      │                │<─ Email settings ─────────────────┤
      │                │                  │                │
      │                ├─ Create Email    │                │
      │                │  From: noreply@org.se             │
      │                │  To: customer@example.se          │
      │                │  Subject: "Faktura XXX"           │
      │                │  Attach: PDF                      │
      │                │                  │                │
      │                ├─ Send Email ────>│                │
      │                │  POST /v3/mail/send               │
      │                │                  │                │
      │                │                  ├─ Process       │
      │                │                  │  (SendGrid)    │
      │                │                  │                │
      │                │<─ 202 Accepted ──┤                │
      │                │  {messageId}     │                │
      │                │                  │                │
      │                ├─ Update Metadata ────────────────>│
      │                │  deliveryAttempts.push({          │
      │                │    channel: "email",              │
      │                │    status: "delivered",           │
      │                │    messageId,                     │
      │                │    timestamp                      │
      │                │  })                               │
      │                │<─ Updated ────────────────────────┤
      │                │                  │                │
      │<─ ACK (delete)─┤                  │                │
      │                │                  │                │
      │                                                    │
      │  IF SENDGRID FAILS (429 Rate Limit):               │
      │                │                  │                │
      │                │<─ 429 Too Many ──┤                │
      │                │  Retry-After: 60s│                │
      │                │                  │                │
      │                ├─ Re-queue Message                 │
      │<─ Re-enqueue ──┤  visibilityTimeout=60s            │
      │  (retry)       │                  │                │
      │                                                    │
      │  IF ALL RETRIES FAIL → FALLBACK TO POSTAL:         │
      │                │                  │                │
      │                ├─ Enqueue to postal-bulk-queue     │
      │                │  (fallback delivery)              │

...

5.6 Postal Bulk Processing Flow (21G Integration)

┌──────────────────────────────────────────────────────────┐
│      Postal Bulk Service: 21G Integration Flow           │
└──────────────────────────────────────────────────────────┘

Scheduled Trigger    Postal Service       21G SFTP         Blob Storage
(12:00, 20:00 CET)         │                  │                │
      │                    │                  │                │
      ├─ CRON Trigger ────>│                  │                │
      │                    │                  │                │
      │                    ├─ Fetch All Messages from          │
      │                    │  postal-bulk-queue                │
      │                    │  (batch retrieval)                │
      │                    │  Result: 150 invoices             │
      │                    │                  │                │
      │                    ├─ Group by Organization            │
      │                    │  Org A: 100 invoices              │
      │                    │  Org B: 50 invoices               │
      │                    │                  │                │
      │                    ├─ FOR Org A:      │                │
      │                    │                  │                │
      │                    ├─ Download 100 PDFs ──────────────>│
      │                    │  GET {org-a}-invoices-2025/       │
      │                    │      11/21/{id}.pdf               │
      │                    │<─ PDF Bytes (100×) ───────────────┤
      │                    │                  │                │
      │                    ├─ Create 21G XML Metadata          │
      │                    │  <PrintBatch>                     │
      │                    │    <TotalDocuments>100            │
      │                    │    <Documents>                    │
      │                    │      <Document>                   │
      │                    │        <DocumentId>001.pdf        │
      │                    │        <Recipient>                │
      │                    │          <Name>...</Name>         │
      │                    │          <Address>...</Address>   │
      │                    │                  │                │
      │                    ├─ Create ZIP Archive               │
      │                    │  ORGA_20251121_001.zip            │
      │                    │  ├── metadata.xml                 │
      │                    │  ├── invoice_001.pdf              │
      │                    │  ├── invoice_002.pdf              │
      │                    │  └── ... (100 PDFs)               │
      │                    │                  │                │
      │                    ├─ Upload to 21G ─>│                │
      │                    │  SFTP PUT        │                │
      │                    │  /incoming/ORGA/ │                │
      │                    │  ORGA_20251121_  │                │
      │                    │  001.zip         │                │
      │                    │<─ Upload Success ┤                │
      │                    │                  │                │
      │                    ├─ Update Invoice Statuses ────────>│
      │                    │  (100 invoices)                   │
      │                    │  status="postal_sent"             │
      │                    │<─ Updated (100×) ─────────────────┤
      │                    │                  │                │
      │                    ├─ Delete Queue Messages            │
      │                    │  (100 messages from queue)        │
      │                    │                  │                │
      │                    ├─ Send Notification Email          │
      │                    │  To: ops@org-a.com                │
      │                    │  Subject: "21G Batch Sent"        │
      │                    │  Body: "100 invoices"             │
      │                    │                  │                │
      │                    ├─ Log to App Insights              │
      │                    │  Metric: Postal.BatchSize=100     │

...

5.7 Error Handling & Retry Flow

┌─────────────────────────────────────────────────────────┐
│          Error Handling & Retry Flow Diagram            │
└─────────────────────────────────────────────────────────┘

Processing      Error Occurs       Retry Logic        Final State
    │                │                  │                   │
    ├─ Render HTML   │                  │                   │
    │                X                  │                   │
    │           Template error          │                   │
    │           (missing variable)      │                   │
    │                │                  │                   │
    │                ├─ Log Error ─────>│                   │
    │                │  Application     │                   │
    │                │  Insights        │                   │
    │                │  Level: Error    │                   │
    │                │  CorrelationId   │                   │
    │                │                  │                   │
    │                ├─ Increment ─────>│                   │
    │                │  retryCount=1    │                   │
    │                │                  │                   │
    │                ├─ Retry Attempt 1 │                   │
    │                │  Wait: 60 seconds│                   │
    │                X Still fails      │                   │
    │                │                  │                   │
    │                ├─ Increment ─────>│                   │
    │                │  retryCount=2    │                   │
    │                │                  │                   │
    │                ├─ Retry Attempt 2 │                   │
    │                │  Wait: 5 minutes │                   │
    │                X Still fails      │                   │
    │                │                  │                   │
    │                ├─ Increment ─────>│                   │
    │                │  retryCount=3    │                   │ 
    │                │                  │                   │
    │                ├─ Retry Attempt 3 │                   │
    │                │  Wait: 15 minutes│                   │
    │                X Still fails      │                   │
    │                │                  │                   │
    │                ├─ Max Retries ───────────────────────>│
    │                │  Exceeded        │           poison-queue
    │                │                  │                   │
    │                ├─ Create Poison ─────────────────────>│
    │                │  Message with:   │           {       │
    │                │  • Original msg  │    retryCount: 3  │
    │                │  • All errors    │    lastError      │
    │                │  • Timestamps    │    alertSent      │
    │                │                  │   }               │
    │                │                  │                   │
    │                ├─ Send Alert ────────────────────────>│
    │                │  Email to:       │    support@egflow.com
    │                │  support team    │    Subject: "Poison Queue"
    │                │                  │                   │
    │                ├─ Update Batch ──────────────────────>│
    │                │  failedItems++   │    Batch metadata │
    │                │  errors.push()   │    updated        │
    │                │                  │                   │
    │<─ Delete from ─┤                  │                   │
    │   main queue   │                  │                   │

...

5.8 Multi-Vendor Transformation Flow

┌──────────────────────────────────────────────────────────┐
│     Multi-Vendor XML → Canonical JSON Transformation     │
└──────────────────────────────────────────────────────────┘

GASEL XML                     Parser                  Canonical JSON
    │                            │                          │
<InvoiceBatch                     │                          │
 xmlns="urn:ediel:...">           │                          │
  <Invoice>                       │                          │
    <InvoiceHeader>               │                          │
      <InvoiceNumber>             │                          │
        2025-11-001               │                          │
      </InvoiceNumber>            │                          │
    <CustomerParty>               │                          │
      <PartyName>                 │                          │
        Medeni Schröder           │                          │
      </PartyName>                │                          │
    <MonetarySummary>             │                          │
      <PayableAmount>             │                          │
        749.28                    │                          │
      </PayableAmount>            │                          │
    ├────────────────────────────>│                          │
    │                            ├─ Detect: GASEL           │
    │                            │  (namespace match)       │
    │                            │                          │
    │                            ├─ Load Mapping ──────────>│
    │                            │  gasel-mapping.json      │
    │                            │                          │
    │                            ├─ Extract via XPath:     │
    │                            │  invoiceNumber =         │
    │                            │    "InvoiceHeader/       │
    │                            │     InvoiceNumber"       │
    │                            │  customerName =          │
    │                            │    "CustomerParty/       │
    │                            │     PartyName"           │
    │                            │                          │
    │                            ├─ Transform ─────────────>│
    │                            │                  {       │
    │                            │    "invoiceNumber": "2025-11-001",
    │                            │    "customer": {
    │                            │      "fullName": "Medeni Schröder"
    │                            │    },
    │                            │    "invoiceDetails": {
    │                            │      "totalAmount": 749.28
    │                            │    },
    │                            │    "sourceMetadata": {
    │                            │      "vendorCode": "GASEL"
    │                            │    }
    │                            │  }

XELLENT XML                   Parser                  Canonical JSON
    │                            │                          │
<InvoiceBatch                     │                          │
 xmlns="http://oio.dk..."         │                          │
 xmlns:com="...common">           │                          │
  <Invoice>                       │                          │
    <com:ID>                      │                          │
      5002061556                  │                          │
    </com:ID>                    │                          │
    <com:BuyerParty>             │                          │
      <com:PartyName>            │                          │
        <com:Name>               │                          │
          Erik Svensson          │                          │
        </com:Name>              │                          │
    <com:LegalTotals>            │                          │
      <com:ToBePaidTotalAmount>  │                          │
        2 567,00                 │                          │
    ├────────────────────────────>│                          │
    │                            ├─ Detect: XELLENT        │
    │                            │  (oio.dk namespace)      │
    │                            │                          │
    │                            ├─ Load Mapping           │
    │                            │  xellent-mapping.json    │
    │                            │                          │
    │                            ├─ Handle Namespaces      │
    │                            │  (com:, main: prefixes)  │
    │                            │                          │
    │                            ├─ Extract via XPath:     │
    │                            │  invoiceNumber = "com:ID"│
    │                            │  customerName =          │
    │                            │    "com:BuyerParty/      │
    │                            │     com:PartyName/       │
    │                            │     com:Name"            │
    │                            │                          │
    │                            ├─ Normalize Amount:      │
    │                            │  "2 567,00" → 2567.00    │
    │                            │                          │
    │                            ├─ Transform ─────────────>│
    │                            │                  {       │
    │                            │    "invoiceNumber": "5002061556",
    │                            │    "customer": {
    │                            │      "fullName": "Erik Svensson"
    │                            │    },
    │                            │    "invoiceDetails": {
    │                            │      "totalAmount": 2567.00
    │                            │    },
    │                            │    "sourceMetadata": {
    │                            │      "vendorCode": "XELLENT"
    │                            │    }
    │                            │  }

ZYNERGY XML                   Parser                  Canonical JSON
    │                            │                          │
<InvoiceBatch                     │                          │
 xmlns="http://eg.dk/Zynergy">    │                          │
  <Invoice>                       │                          │
    <InvoiceData>                 │                          │
      <InvoiceNumber>             │                          │
        100000                    │                          │
      </InvoiceNumber>            │                          │
    <Customer>                    │                          │
      <ReadOnlyFullName>          │                          │
        Alfred Asplund            │                          │
      </ReadOnlyFullName>         │                          │
    <InvoiceData>                 │                          │
      <InvoiceAmount>             │                          │
        1056.00                   │                          │
    ├────────────────────────────>│                          │
    │                            ├─ Detect: ZYNERGY        │
    │                            │  (Zynergy namespace)     │
    │                            │                          │
    │                            ├─ Load Mapping           │
    │                            │  zynergy-mapping.json    │
    │                            │                          │
    │                            ├─ Extract via XPath:     │
    │                            │  invoiceNumber =         │
    │                            │    "InvoiceData/         │
    │                            │     InvoiceNumber"       │
    │                            │  customerName =          │
    │                            │    "Customer/            │
    │                            │     ReadOnlyFullName"    │
    │                            │                          │
    │                            ├─ Transform ─────────────>│
    │                            │                  {       │
    │                            │    "invoiceNumber": "100000",
    │                            │    "customer": {
    │                            │      "fullName": "Alfred Asplund"
    │                            │    },
    │                            │    "invoiceDetails": {
    │                            │      "totalAmount": 1056.00
    │                            │    },
    │                            │    "sourceMetadata": {
    │                            │      "vendorCode": "ZYNERGY"
    │                            │    }
    │                            │  }
                                 │                          │
                            All 3 formats           Single standard
                            produce same      ───>   canonical JSON
                            structure                for downstream

...

5.9 Distribution Routing Decision Flow

┌─────────────────────────────────────────────────────────┐
│         Distribution Channel Routing Logic              │
└─────────────────────────────────────────────────────────┘

Invoice Data          Routing Logic             Queue Selection
      │                     │                          │
      ├─ Check Customer    │                          │
      │  Preference         │                          │
      │                     │                          │
      │  preference=        │                          │
      │  "postal"? ─────────┤                          │
      │                     ├─ YES ───────────────────>│
      │                     │                   postal-bulk-queue
      │                     │                          │
      │                     ├─ NO                      │
      │                     │  Continue...             │
      │                     │                          │
      ├─ Load Org          │                          │
      │  Channel Priority   │                          │
      │  [email, postal]    │                          │
      │                     │                          │
      │                     ├─ TRY Priority 1: email  │
      │                     │                          │
      ├─ Has email         │                          │
      │  address?           │                          │
      │                     │                          │
      │  email != null? ────┤                          │
      │                     ├─ YES                     │
      │                     │  Validate email format   │
      │                     │                          │
      │  valid email? ──────┤                          │
      │                     ├─ YES ───────────────────>│
      │                     │                   email-queue
      │                     │                          │
      │                     ├─ NO (invalid/missing)    │
      │                     │  Try next channel...     │
      │                     │                          │
      │                     ├─ TRY Priority 2: postal │
      │                     │                          │
      ├─ Complete          │                          │
      │  address?           │                          │
      │                     │                          │
      │  address valid? ────┤                          │
      │                     ├─ YES ───────────────────>│
      │                     │                   postal-bulk-queue
      │                     │                          │
      │                     ├─ NO (incomplete)         │
      │                     │  Log error               │
      │                     │  Skip invoice            │
      │                     │  Alert organization      │
      │                     │                          │
      │  Swedish Law:       │                          │
      │  "Rätt till         │                          │
      │  pappersfaktura"    │                          │
      │  Always ensure      │                          │
      │  postal available ──┤                          │

...

6. API Specifications

6.1 API Design Principles

Standards:

  • RESTful design with resource-based URLs
  • JSON request/response bodies
  • HTTP status codes per RFC 7231
  • OAuth 2.0 Bearer token authentication
  • URL path versioning (/v1, /v2)
  • Consistent error response envelope
  • HATEOAS links for related resources (optional)

Base URL: https://api.egflow.com/v1

Response Envelope (All Responses):

{
  "success": true|false,
  "data": { },
  "metadata": {
    "timestamp": "ISO8601",
    "requestId": "uuid",
    "apiVersion": "1.0"
  },
  "errors": null|[]
}

...

6.2 Complete API Endpoint Catalog

6.2.1 Batch Management APIs

MethodEndpointDescriptionAuth RoleRate Limit
POST/v1/organizations/{orgId}/batchesUpload batch XMLBatch Operator10/hour
POST/v1/organizations/{orgId}/batches/{batchId}/startStart processingBatch Operator30/hour
GET/v1/organizations/{orgId}/batches/{batchId}Get batch statusRead-Only100/min
GET/v1/organizations/{orgId}/batchesList batches (with filters)Read-Only100/min
GET/v1/organizations/{orgId}/batches/{batchId}/itemsList invoice itemsRead-Only100/min
GET/v1/organizations/{orgId}/batches/{batchId}/items/{itemId}Get item detailsRead-Only100/min
PUT/v1/organizations/{orgId}/batches/{batchId}Update batch metadataBatch Operator30/hour

6.2.2 Organization APIs

MethodEndpointDescriptionAuth RoleRate Limit
GET/v1/organizations/{orgId}Get organization detailsRead-Only100/min
PUT/v1/organizations/{orgId}Update organization configOrg Admin10/hour

6.2.3 Template APIs

MethodEndpointDescriptionAuth RoleRate Limit
GET/v1/organizations/{orgId}/templatesList templates (filter: category, status)Read-Only100/min
GET/v1/organizations/{orgId}/templates/{templateId}Get template detailsRead-Only100/min
GET/v1/organizations/{orgId}/template-categoriesList template categoriesRead-Only100/min

6.2.4 Schema Management APIs

MethodEndpointDescriptionAuth RoleRate Limit
GET/v1/organizations/{orgId}/schemasList supported vendor formatsRead-Only100/min
POST/v1/organizations/{orgId}/schemas/validatePre-validate XMLBatch Operator20/hour

6.2.5 Invoice APIs

MethodEndpointDescriptionAuth RoleRate Limit
GET/v1/organizations/{orgId}/invoices/{invoiceId}/pdfDownload PDFRead-Only1000/hour
GET/v1/organizations/{orgId}/invoices/{invoiceId}/htmlDownload HTMLRead-Only1000/hour

6.2.6 System APIs

MethodEndpointDescriptionAuth RoleRate Limit
GET/v1/healthHealth checkNoneUnlimited
GET/v1/versionAPI version infoNoneUnlimited

6.3 Detailed API Specifications

6.3.1 POST /organizations/{orgId}/batches (Batch Upload)

Purpose: Upload batch invoice XML file for processing

Request Headers:

Authorization: Bearer {jwt-token}
Content-Type: multipart/form-data

...

Request Body:

file: [XML file binary]
metadata: {
  "batchName": "Invoice_November_2025",
  "priority": "normal"
}

...

Success Response (201 Created):

{
  "success": true,
  "data": {
    "batchId": "550e8400-e29b-41d4-a716-446655440000",
    "organizationId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "uploaded",
    "uploadedAt": "2025-11-21T10:30:00Z",
    "fileInfo": {
      "fileName": "invoices_nov.xml",
      "fileSize": 15728640,
      "checksum": "sha256:a3d5e7f9...",
      "detectedFormat": "GASEL"
    },
    "blobPath": "acme-batches-2025/11/21/550e8400.../source.xml"
  }
}

...

Error Responses:

// 400 Bad Request - Invalid XML
{
  "success": false,
  "errors": [{
    "code": "INVALID_XML",
    "message": "XML file is not well-formed",
    "field": "file",
    "details": {
      "line": 142,
      "column": 23,
      "error": "Unexpected end tag: </Invoice>",
      "suggestion": "Check that all opening tags have matching closing tags",
      "documentationUrl": "https://docs.egflow.com/errors/INVALID_XML"
    }
  }]
}

// 413 Payload Too Large
{
  "success": false,
  "errors": [{
    "code": "FILE_TOO_LARGE",
    "message": "File exceeds 100MB limit",
    "details": {
      "fileSize": 105906176,
      "limit": 104857600,
      "suggestion": "Split large batches into multiple files"
    }
  }]
}

// 415 Unsupported Media Type
{
  "success": false,
  "errors": [{
    "code": "UNSUPPORTED_FORMAT",
    "message": "Cannot detect vendor format",
    "details": {
      "detectedNamespace": "http://unknown.com/schema",
      "supportedFormats": ["GASEL", "XELLENT", "ZYNERGY"],
      "suggestion": "Ensure XML uses one of the supported vendor formats",
      "documentationUrl": "https://docs.egflow.com/vendor-formats"
    }
  }]
}

// 429 Too Many Requests
{
  "success": false,
  "errors": [{
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many batch uploads",
    "details": {
      "limit": 10,
      "window": "1 hour",
      "retryAfter": "2025-11-21T11:30:00Z"
    }
  }]
}

...

Response Headers:

X-RateLimit-Limit: 10
X-RateLimit-Remaining: 5
X-RateLimit-Reset: 1700226000
Location: /v1/organizations/{orgId}/batches/{batchId}

...

6.3.2 POST /organizations/{orgId}/batches/{batchId}/start

Purpose: Start asynchronous processing of uploaded batch

Request:

{
  "validationMode": "strict"
}

...

Success Response (202 Accepted):

{
  "success": true,
  "data": {
    "batchId": "550e8400-e29b-41d4-a716-446655440000",
    "status": "queued",
    "queuedAt": "2025-11-21T10:35:00Z",
    "estimatedProcessingTime": "15-30 minutes",
    "queuePosition": 2
  }
}

...

Error Responses:

// 409 Conflict - Already Processing
{
  "success": false,
  "errors": [{
    "code": "CONFLICT",
    "message": "Batch is already processing",
    "details": {
      "currentStatus": "processing",
      "startedAt": "2025-11-21T10:00:00Z",
      "estimatedCompletionAt": "2025-11-21T11:30:00Z"
    }
  }]
}

// 503 Service Unavailable - Queue Full
{
  "success": false,
  "errors": [{
    "code": "SERVICE_UNAVAILABLE",
    "message": "System at capacity, retry later",
    "details": {
      "queueDepth": 10500,
      "estimatedWaitTime": "30-60 minutes",
      "retryAfter": "2025-11-21T11:00:00Z",
      "suggestion": "Consider scheduling batch for off-peak hours (22:00-06:00 CET)"
    }
  }]
}

...

6.3.3 GET /organizations/{orgId}/batches/{batchId}

Purpose: Get current batch status and statistics

Success Response (200 OK):

{
  "success": true,
  "data": {
    "batchId": "550e8400-e29b-41d4-a716-446655440000",
    "organizationId": "123e4567-e89b-12d3-a456-426614174000",
    "batchName": "Invoice_November_2025",
    "status": "processing",
    "priority": "normal",
    "vendorInfo": {
      "vendorCode": "GASEL",
      "vendorName": "Telinet Energi / EDIEL",
      "version": "1.0",
      "detectedNamespace": "urn:ediel:se:electricity:invoice:1.0"
    },
    "statistics": {
      "totalItems": 5000,
      "parsedItems": 5000,
      "queuedItems": 1800,
      "processingItems": 200,
      "completedItems": 3000,
      "failedItems": 0,
      "successRate": 100.0,
      "itemsByStatus": {
        "queued": 1800,
        "rendering": 50,
        "rendered": 100,
        "delivering": 50,
        "delivered": 2900,
        "failed": 0
      },
      "deliveryChannelBreakdown": {
        "email": 2500,
        "postal": 500
      }
    },
    "timestamps": {
      "uploadedAt": "2025-11-21T10:30:00Z",
      "queuedAt": "2025-11-21T10:35:00Z",
      "startedAt": "2025-11-21T10:35:10Z",
      "estimatedCompletionAt": "2025-11-21T11:05:00Z",
      "completedAt": null
    },
    "fileInfo": {
      "fileName": "invoices_nov.xml",
      "fileSize": 15728640,
      "format": "xml"
    }
  }
}

...

6.3.4 GET /organizations/{orgId}/batches

Purpose: List and search batches with filtering

Query Parameters:

  • from: ISO 8601 date (default: 90 days ago)
  • to: ISO 8601 date (default: today)
  • status: uploaded|queued|processing|completed|failed
  • vendorCode: GASEL|XELLENT|ZYNERGY
  • search: Batch name search
  • sortBy: uploadedAt|completedAt|batchName
  • order: asc|desc (default: desc)
  • page: integer (default: 1)
  • pageSize: integer (default: 50, max: 500)

Success Response (200 OK):

{
  "success": true,
  "data": {
    "batches": [
      {
        "batchId": "uuid",
        "batchName": "Invoice_November_2025",
        "status": "completed",
        "vendorCode": "GASEL",
        "statistics": {
          "totalItems": 5000,
          "successfulItems": 4950,
          "failedItems": 50
        },
        "timestamps": {
          "uploadedAt": "2025-11-21T10:30:00Z",
          "completedAt": "2025-11-21T11:45:00Z"
        }
      }
    ],
    "pagination": {
      "currentPage": 1,
      "pageSize": 50,
      "totalBatches": 127,
      "totalPages": 3,
      "hasNextPage": true,
      "hasPreviousPage": false
    }
  }
}

...

6.3.5 GET /organizations/{orgId}/batches/{batchId}/items

Purpose: List individual invoice items in batch

Query Parameters:

  • status: queued|processing|completed|failed
  • page: integer (default: 1)
  • pageSize: integer (default: 50, max: 500)

Success Response (200 OK):

{
  "success": true,
  "data": {
    "items": [
      {
        "itemId": "uuid",
        "batchId": "uuid",
        "invoiceNumber": "2025-11-001",
        "customerReference": "020624-2380",
        "customerName": "Medeni Schröder",
        "totalAmount": 749.28,
        "currency": "SEK",
        "status": "delivered",
        "deliveryChannel": "email",
        "processedAt": "2025-11-21T10:45:00Z",
        "deliveredAt": "2025-11-21T10:46:15Z"
      }
    ],
    "pagination": {
      "currentPage": 1,
      "pageSize": 50,
      "totalItems": 5000,
      "totalPages": 100
    }
  }
}

...

6.3.6 GET /organizations/{orgId}/batches/{batchId}/items/{itemId}

Purpose: Get detailed invoice item information

Success Response (200 OK):

{
  "success": true,
  "data": {
    "itemId": "uuid",
    "batchId": "uuid",
    "organizationId": "uuid",
    "invoiceNumber": "2025-11-001",
    "invoiceDate": "2025-11-06",
    "dueDate": "2025-11-20",
    "currency": "SEK",
    "customerInfo": {
      "customerId": "020624-2380",
      "fullName": "Medeni Schröder",
      "email": "muntaser.af@zavann.net",
      "phone": "09193538799"
    },
    "invoiceDetails": {
      "subTotal": 599.42,
      "taxAmount": 149.86,
      "totalAmount": 749.28
    },
    "status": "delivered",
    "deliveryChannel": "email",
    "deliveryStatus": {
      "attemptedAt": "2025-11-21T10:46:00Z",
      "deliveredAt": "2025-11-21T10:46:15Z",
      "providerMessageId": "sendgrid-msg-12345"
    },
    "processingTimeline": [
      {"status": "queued", "timestamp": "2025-11-21T10:35:00Z"},
      {"status": "rendering", "timestamp": "2025-11-21T10:44:00Z"},
      {"status": "rendered", "timestamp": "2025-11-21T10:45:00Z"},
      {"status": "delivering", "timestamp": "2025-11-21T10:46:00Z"},
      {"status": "delivered", "timestamp": "2025-11-21T10:46:15Z"}
    ],
    "sourceInfo": {
      "vendorCode": "GASEL",
      "originalInvoiceId": "2025-11-001"
    },
    "fileReferences": {
      "pdfUrl": "/v1/organizations/{orgId}/invoices/{itemId}/pdf",
      "htmlUrl": "/v1/organizations/{orgId}/invoices/{itemId}/html"
    }
  }
}

...

6.3.7 GET /organizations/{orgId}/schemas/validate

Purpose: Pre-validate XML before upload

Request:

{
  "xmlContent": "<?xml version=\"1.0\"?>\n<InvoiceBatch>...</InvoiceBatch>",
  "vendorCode": "GASEL"
}

...

Success Response (200 OK):

{
  "success": true,
  "data": {
    "valid": true,
    "vendorCode": "GASEL",
    "version": "1.0",
    "invoiceCount": 10,
    "batchId": "BATCH2025110600001",
    "validationDetails": {
      "schemaValid": true,
      "structureValid": true,
      "requiredFieldsPresent": true
    },
    "warnings": [
      {
        "field": "CustomerParty[0]/Contact/ElectronicMail",
        "message": "Email format should be validated",
        "severity": "warning",
        "line": 45
      }
    ],
    "errors": []
  }
}

...

7. Error Handling & Validation

7.1 Comprehensive Field Validation Matrix

7.1.1 Customer Information Validation

FieldTypeMinMaxFormatRequiredValidation
customerIdString150Alphanumeric, dash, underscoreYes`^[A-Za-z0-9_-]+
personnummerString1013YYMMDD-XXXX or YYYYMMDD-XXXXConditionalLuhn algorithm
fullNameString1255Unicode printableYesNot empty, trim
emailString5255RFC 5322NoRegex + optional DNS MX
phoneString820E.164 recommendedNo`^+?[0-9\s-]+
streetString1255Any printableYes (postal)Not empty
postalCodeString510Country-specificYes (postal)Swedish: `^\d{3}\s?\d{2}
cityString1100Any printableYes (postal)Not empty
countryString22ISO 3166-1 alpha-2YesEnum: SE, NO, DK, FI

7.1.2 Financial Data Validation

FieldTypeMinMaxDecimalsRequiredValidation
subTotalDecimal0.00999999999.992Yes≥ 0
taxAmountDecimal0.00999999999.992Yes≥ 0
totalAmountDecimal0.01999999999.992Yes> 0
unitPriceDecimal0.00999999.992-6Yes≥ 0
quantityDecimal0.01999999.992Yes> 0
taxRateDecimal01001YesSwedish: 0, 6, 12, 25

7.1.3 Business Logic Validation Rules

RuleLogicError CodeMessage
Total ConsistencytotalAmount == subTotal + taxAmountAMOUNT_MISMATCHTotal must equal subtotal plus tax
Line Items Sumsum(lineItems.lineAmount) == subTotalLINE_ITEMS_MISMATCHLine items must sum to subtotal
Date LogicdueDate >= invoiceDateINVALID_DATE_RANGEDue date must be on or after invoice date
Tax Rate ValidtaxRate in [0, 6, 12, 25]INVALID_TAX_RATESwedish VAT: 0%, 6%, 12%, or 25%
Currency MatchAll amounts same currencyCURRENCY_MISMATCHAll amounts must use same currency
Personnummer LuhnLuhn checksumINVALID_PERSONNUMMERInvalid Swedish personnummer
Swedish Postal CodeFormat XXX XXINVALID_POSTAL_CODEFormat must be: XXX XX

7.2 Error Handling Scenarios

7.2.1 Scenario: Malformed XML Upload

Trigger: User uploads non-well-formed XML

System Action:

  1. XML parser throws exception during parse attempt
  2. Catch exception, extract line/column from error
  3. Do NOT store file in blob storage
  4. Log error to Application Insights with file details (no content)
  5. Return 400 Bad Request with detailed error

Response:

{
  "success": false,
  "errors": [{
    "code": "INVALID_XML",
    "message": "XML file is not well-formed",
    "field": "file",
    "details": {
      "line": 142,
      "column": 23,
      "error": "Unexpected end tag: </Invoice>. Expected: </InvoiceHeader>",
      "suggestion": "Verify all XML tags are properly closed and nested",
      "documentationUrl": "https://docs.egflow.com/xml-format"
    }
  }]
}

...

7.2.2 Scenario: Unsupported Vendor Format

Trigger: XML namespace doesn't match GASEL, XELLENT, or ZYNERGY

System Action:

  1. Store file in blob for manual review
  2. Update batch status to "failed"
  3. Log unsupported format to Application Insights
  4. Send alert to support team
  5. Return 415 Unsupported Media Type

Response:

{
  "success": false,
  "errors": [{
    "code": "UNSUPPORTED_FORMAT",
    "message": "Cannot detect vendor format",
    "details": {
      "detectedNamespace": "http://custom-vendor.com/invoices",
      "rootElement": "InvoiceBatch",
      "supportedFormats": [
        {
          "vendorCode": "GASEL",
          "namespace": "urn:ediel:se:electricity:invoice:1.0",
          "description": "Telinet Energi / EDIEL format"
        },
        {
          "vendorCode": "XELLENT",
          "namespace": "http://rep.oio.dk/ubl/xml/schemas/0p71/pie/",
          "description": "Karlskoga Energi / OIOXML format"
        },
        {
          "vendorCode": "ZYNERGY",
          "namespace": "http://eg.dk/Zynergy/1.0/invoice.xsd",
          "description": "EG Software Zynergy format"
        }
      ],
      "suggestion": "Contact EG Support to add support for your vendor format",
      "supportEmail": "support@egflow.com"
    }
  }]
}

...

7.2.3 Scenario: Template Rendering Failure

Trigger: Handlebars template references undefined variable

System Action:

1. DocumentGenerator attempts to render template
2. Handlebars throws exception: Variable 'customer.address.street' not found
3. Log error with full context (template24h to authorities)<br>- Security measures documentation<br>- Annual security audit<br>- CISO designated | Legal/Compliance |
| Swedish Säkerhetspolisen (SÄPO) requirements | LOW | HIGH | - Enhanced security for critical infrastructure<br>- Incident reporting to MSB (Swedish Civil Contingencies)<br>- Employee background checks for production access<br>- Security clearance for key personnel | Security Officer |
| API key theft/leakage | MEDIUM | HIGH | - Rotate keys every 90 days<br>- Monitor for leaked keys (GitHub scanning)<br>- Revoke compromised keys immediately<br>- API key hashing in database<br>- Never log full API keys | Security Officer |
| Insider threat (privileged access abuse) | LOW | CRITICAL | - Least privilege principle<br>- All actions audited<br>- Regular access reviews<br>- Separation of duties<br>- Anomaly detection in audit logs | Security Officer |
| Third-party vendor breach (SendGrid, 21G) | LOW | HIGH | - Data Processing Agreements (DPAs) signed<br>- Regular vendor security assessments<br>- Minimal data sharing<br>- Encryption in transit to vendors<br>- Vendor breach response plan | Legal/Compliance |

---

## 4.5 NFR-005: Data Retention & Lifecycle Management

**Requirement:** The system shall manage data retention according to Swedish Bokföringslagen (7-year invoice retention) with automated lifecycle policies for cost optimization through storage tier transitions.

**Priority:** **HIGH**

**Retention Policies:**

| Data Type | Legal Requirement | Retention Period | Storage Tier Transition | Disposal Method |
|-----------|------------------|-----------------|------------------------|-----------------|
| **Invoices (PDF/HTML/JSON)** | Bokföringslagen (Swedish Accounting Act) | 7 years from fiscal year end | Day 0-365: Hot<br>Day 366-2555: Cool<br>Day 2556+: Archive | Permanent deletion after 7 years |
| **Batch Source Files (XML)** | None (internal processing) | 90 days | Day 0-30: Hot<br>Day 31-90: Cool<br>Day 91+: Delete | Automatic deletion |
| **Batch Metadata JSON** | Audit trail | 90 days | Day 0-90: Hot<br>Day 91+: Delete | Automatic deletion |
| **Audit Logs (PostgreSQL)** | GDPR, Swedish law | 7 years | Year 0-1: PostgreSQL<br>Year 1-7: Blob (compressed) | Deletion after 7 years |
| **Application Logs** | Operational | 90 days | Application Insights | Automatic deletion |
| **Templates** | Business continuity | Indefinite (archived versions) | Hot (active)<br>Cool (archived) | Never deleted |
| **Organization Config** | Business continuity | Indefinite | Hot | Never deleted (updated in place) |

**Azure Blob Lifecycle Policy:**

```json
{
  "rules": [
    {
      "enable---

## 4.3 NFR-003: Availability & Reliability (Nordic 24/7 Operations)

**Requirement:** The system shall maintain 99.9% uptime with automatic failover, multi-region deployment, and recovery procedures to support Nordic utilities' 24/7 invoice delivery operations.

**Priority:** **HIGH**

**Availability Targets:**

| Metric | Target | Allowed Downtime | Measurement | Consequences of Breach |
|--------|--------|-----------------|-------------|----------------------|
| **System Uptime** | 99.9% | 43 min/month | Azure Monitor | SLA credit to customers |
| **Batch Success Rate** | > 99.5% | 50 failures per 10K | Processing logs | Investigation required |
| **Delivery Success Rate** | > 98% | 200 failures per 10K | Delivery tracking | Alert to organization |
| **API Availability** | 99.9% | 43 min/month | Health check monitoring | Incident escalation |
| **MTTR (Mean Time To Recovery)** | < 30 minutes | N/A | Incident timestamps | Process improvement |
| **MTBF (Mean Time Between Failures)** | > 720 hours (30 days) | N/A | Incident tracking | Root cause analysis |

**Multi-Region Deployment:**

...

Primary Region: West Europe (Azure westeurope)

  • Sweden: Primary processing
  • Denmark: Primary processing

Secondary Region: North Europe (Azure northeurope)

  • Norway: Primary processing
  • Finland: Primary processing
  • Failover for Sweden/Denmark

Traffic Routing:

  • Azure Traffic Manager with Performance routing
  • Health check: /health endpoint every 30 seconds
  • Auto-failover on 3 consecutive failed health checks
  • Failover time: < 2 minutes

**Recovery Time Objectives:**

| Scenario | RTO (Recovery Time) | RPO (Data Loss) | Recovery Method | Responsible Team |
|----------|---------------------|-----------------|-----------------|------------------|
| **Worker Instance Crash** | < 5 minutes | 0 (idempotent) | Automatic queue retry | Automatic |
| **Database Failure** | < 15 minutes | < 5 minutes | Auto-failover to read replica | Automatic + Ops verification |
| **Primary Region Failure** | < 30 minutes | < 15 minutes | Traffic Manager failover to secondary region | Ops Manager |
| **Blob Storage Corruption** | < 1 hour | < 1 hour | Restore from blob version/snapshot | Ops Team |
| **Queue Service Outage** | < 15 minutes | 0 (messages preserved) | Wait for Azure recovery | Ops Manager |
| **SendGrid Complete Outage** | < 2 hours | 0 (fallback to postal) | Route all to postal queue | Ops Team |
| **21G SFTP Unavailable** | < 4 hours | 0 (retry at next scheduled run) | Retry at 12:00 or 20:00 | Ops Team |

**Backup & Recovery Strategy:**

**Blob Storage:**
```yaml
Replication: Geo-Redundant Storage (GRS)
  - Primary: West Europe
  - Secondary: North Europe
  - Automatic replication

Soft Delete: 7 days retention
  - Recover accidentally deleted blobs within 7 days

Blob Versioning: 30 days retention
  - Previous versions accessible
  - Rollback capability

Point-in-Time Restore: Not needed (versioning sufficient)

...

PostgreSQL:

Backup Schedule: Daily automated backups
Retention: 35 days
Backup Window: 02:00-04:00 CET (low traffic period)
Point-in-Time Restore: 7 days
Geo-Redundant: Enabled
Read Replica: North Europe (for failover)

...

Acceptance Criteria:

CriterionValidation MethodTarget
Multi-region deployment operationalVerify services in both regionsBoth regions active
Traffic Manager routes to healthy regionSimulate West Europe failureRoutes to North Europe
Database auto-failover testedSimulate primary DB failureFailover < 15 min
Blob geo-replication verifiedWrite to primary, read from secondaryData replicated
Health checks on all servicesGET /health on all endpointsAll return 200
Automated incident alerts configuredSimulate service failureAlert received within 5 min
Worker auto-restart on crashKill worker processNew instance starts
Queue message retry testedSimulate worker crash mid-processingMessage reprocessed
Disaster recovery drill quarterlySimulate complete region lossRecovery within RTO
Backup restoration tested monthlyRestore database from backupSuccessful restore

Dependencies:

  • Azure Traffic Manager configuration
  • Multi-region resource deployment
  • Database replication setup
  • Automated failover testing procedures
  • Incident response runbook

Risks & Mitigation (Nordic Context):

RiskLikelihoodImpactMitigation StrategyOwner
Both Azure regions fail simultaneouslyVERY LOWCRITICAL- Extremely rare (Azure multi-region SLA 99.99%)
- Accept risk (probability vs cost of 3rd region)
- Communication plan for extended outage
- Manual failover to Azure Germany (emergency)
Executive Sponsor
Network partition between regionsLOWHIGH- Each region operates independently
- Eventual consistency acceptable
- Manual reconciliation if partition >1 hour
- Traffic Manager handles routing
Technical Architect
Database failover causes brief downtimeLOWMEDIUM- Accept 1-2 minutes downtime during failover
- API returns 503 with Retry-After
- Queue-based processing unaffected
- Monitor failover duration
Operations Manager
Swedish winter storms affect connectivityLOWMEDIUM- Azure datacenter redundancy within region
- Monitor Azure status dashboard
- Communication plan for customers
- No physical office connectivity required
Operations Manager

4.4 NFR-004: Security Requirements

Requirement: The system shall implement comprehensive security controls including OAuth 2.0 authentication, role-based access control, encryption, audit logging, and protection against OWASP Top 10 vulnerabilities.

Priority: CRITICAL

4.4.1 Authentication & Authorization

OAuth 2.0 Implementation:

Grant Type: Client Credentials Flow (machine-to-machine)
Token Provider: Microsoft Entra ID
Token Lifetime: 1 hour
Refresh Token: 90 days
Token Format: JWT (JSON Web Token)
Algorithm: RS256 (RSA signature with SHA-256)

...

Required Claims in JWT:

{
  "aud": "api://eg-flow-api",
  "iss": "https://login.microsoftonline.com/{tenant}/v2.0",
  "sub": "user-object-id",
  "roles": ["Batch.Operator"],
  "organization_id": "123e4567-e89b-12d3-a456-426614174000",
  "exp": 1700226000,
  "nbf": 1700222400
}

...

Role Definitions & Permissions:

RoleScopePermissionsUse Case
Super AdminGlobal (all organizations)Full CRUD on all resources, cross-org visibilityEG internal support team
Organization AdminSingle organizationManage org users, configure settings, view all batchesUtility IT manager
Template AdminSingle organizationCreate/edit templates, manage template versionsUtility design team
Batch OperatorSingle organizationUpload batches, start processing, view statusUtility billing team
Read-Only UserSingle organizationView batches, download invoices, view reportsUtility customer service
API ClientSingle organizationProgrammatic batch upload and status queriesBilling system integration

Acceptance Criteria:

CriterionValidation MethodTarget
OAuth 2.0 token required for all endpoints (except /health)Call API without token401 Unauthorized
JWT token validated (signature, expiration, audience)Tampered token, expired token401 Unauthorized
Refresh tokens work for 90 daysUse refresh token after 30 daysNew access token issued
All 6 roles implemented in PostgreSQLQuery roles table6 roles present
Users can only access their organizationUser A calls Org B endpoint403 Forbidden
All actions logged to audit_logs tablePerform action, query audit_logsEntry created
API authentication middleware on all routesAttempt bypassAll protected
MFA enforced for Super AdminLogin as Super AdminMFA challenge
MFA enforced for Org AdminLogin as Org AdminMFA challenge
Failed logins logged3 failed login attempts3 entries in audit_logs
Account lockout after 5 failed attempts6 failed login attempts15-minute lockout
API key rotation every 90 daysCheck Key Vault secret ageAlert at 80 days

4.4.2 Data Protection

Encryption Standards:

In Transit:
- TLS 1.3 minimum (TLS 1.2 acceptable)
- Cipher suites: AES-256-GCM, ChaCha20-Poly1305
- Certificate: Wildcard cert for *.egflow.com
- HSTS: max-age=31536000; includeSubDomains

At Rest:
- Azure Blob Storage: AES-256 (Microsoft-managed keys)
- PostgreSQL: AES-256 (Microsoft-managed keys)
- Backups: AES-256 encryption
- Customer-managed keys (CMK): Phase 2 option

Sensitive Data Fields (extra protection):
- Personnummer: Encrypted column in database (if stored)
- API keys: Azure Key Vault only
- Email passwords: Never stored
- Customer addresses: Standard blob encryption sufficient

...

Acceptance Criteria:

CriterionValidation MethodTarget
All API traffic over HTTPSAttempt HTTP requestRedirect to HTTPS or reject
TLS 1.3 or 1.2 enforcedCheck TLS version in trafficTLS ≥ 1.2
Data encrypted at rest (blob)Verify Azure encryption settingsEnabled
Data encrypted at rest (PostgreSQL)Verify DB encryptionEnabled
Secrets in Azure Key Vault onlyCode scan for hardcoded secretsZero secrets in code
No credentials in source controlGit history scanZero credentials
Database connections use managed identityCheck connection stringsNo passwords
Personnummer not in URLsURL pattern analysisNo personnummer patterns
Personnummer not in logsLog analysisNo personnummer found

4.4.3 Application Security (OWASP Top 10)

Security Measures:

OWASP RiskMitigationValidation
A01: Broken Access ControlOrganization middleware, RBAC enforcementPenetration testing
A02: Cryptographic FailuresTLS 1.3, AES-256, Key VaultSecurity scan
A03: InjectionParameterized queries, input validationSQL injection testing
A04: Insecure DesignThreat modeling, security reviewArchitecture review
A05: Security MisconfigurationAzure security baseline, CIS benchmarksConfiguration audit
A06: Vulnerable ComponentsDependabot, automated scanningWeekly scan
A07: Authentication FailuresOAuth 2.0, MFA, rate limitingPenetration testing
A08: Software/Data IntegrityCode signing, SRI, checksumsBuild verification
A09: Logging FailuresComprehensive audit loggingLog completeness review
A10: SSRFURL validation, allowlistSecurity testing

Input Validation:

// Example: Batch upload validation with FluentValidation
public class BatchUploadValidator : AbstractValidator<BatchUploadRequest>
{
    public BatchUploadValidator()
    {
        RuleFor(x => x.File)
            .NotNull().WithMessage("File is required")
            .Must(BeValidXml).WithMessage("File must be valid XML")
            .Must(BeLessThan100MB).WithMessage("File must be less than 100MB");
        
        RuleFor(x => x.Metadata.BatchName)
            .NotEmpty().WithMessage("Batch name is required")
            .Length(1, 255).WithMessage("Batch name must be 1-255 characters")
            .Must(NotContainPathSeparators).WithMessage("Batch name cannot contain / or \\")
            .Must(NoSQLInjectionPatterns).WithMessage("Invalid characters in batch name");
        
        RuleFor(x => x.Metadata.Priority)
            .Must(x => x == "normal" || x == "high")
            .WithMessage("Priority must be 'normal' or 'high'");
    }
    
    private bool NoSQLInjectionPatterns(string input)
    {
        var sqlPatterns = new[] { "--", "/*", "*/", "xp_", "sp_", "';", "\";" };
        return !sqlPatterns.Any(p => input.Contains(p, StringComparison.OrdinalIgnoreCase));
    }
}

...

Acceptance Criteria:

CriterionValidation MethodTarget
Input validation on all API endpointsSend malicious inputRejected with error
SQL injection preventedAttempt SQL injection in batch nameSanitized/rejected
XSS prevented in templatesInject script tags in templateSanitized on render
XML external entity (XXE) attack preventedUpload XXE payloadParsing rejects
Billion laughs attack preventedUpload billion laughs XMLParsing rejects/times out safely
File upload size enforcedUpload 101MB fileRejected at API gateway
Rate limiting prevents abuse1000 rapid API calls429 after limit
CSRF protection (future web UI)Attempt CSRF attackBlocked by token
Dependency vulnerabilities scanned weeklyRun DependabotAlerts for high/critical
Security headers presentCheck HTTP responseX-Frame-Options, CSP, etc.

4.4.4 Network Security

Acceptance Criteria:

CriterionStatusPhase
DDoS protection enabled (Azure basic)✅ IncludedPhase 1
IP whitelisting support for API clients✅ Optional featurePhase 1
VNet integration for Container Apps⚠️ Phase 2Phase 2
Private endpoints for Blob Storage⚠️ Phase 2Phase 2
Network Security Groups (NSGs)⚠️ Phase 2Phase 2
Azure Firewall for egress filtering⚠️ Phase 2Phase 2

Dependencies:

  • FluentValidation library
  • OWASP dependency check tools
  • Penetration testing (external vendor)
  • Security code review process

Risks & Mitigation (Nordic/EU Security Context):

...


...


...