...
- 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
...
- Project Overview
- Business Requirements
- Functional Requirements
- Non-Functional Requirements
- Data Flow Diagrams
- API Specifications
- Error Handling & Validation
- Glossary
- 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
| Method | Endpoint | Description | Auth Role | Rate Limit |
|---|---|---|---|---|
| POST | /v1/organizations/{orgId}/batches | Upload batch XML | Batch Operator | 10/hour |
| POST | /v1/organizations/{orgId}/batches/{batchId}/start | Start processing | Batch Operator | 30/hour |
| GET | /v1/organizations/{orgId}/batches/{batchId} | Get batch status | Read-Only | 100/min |
| GET | /v1/organizations/{orgId}/batches | List batches (with filters) | Read-Only | 100/min |
| GET | /v1/organizations/{orgId}/batches/{batchId}/items | List invoice items | Read-Only | 100/min |
| GET | /v1/organizations/{orgId}/batches/{batchId}/items/{itemId} | Get item details | Read-Only | 100/min |
| PUT | /v1/organizations/{orgId}/batches/{batchId} | Update batch metadata | Batch Operator | 30/hour |
6.2.2 Organization APIs
| Method | Endpoint | Description | Auth Role | Rate Limit |
|---|---|---|---|---|
| GET | /v1/organizations/{orgId} | Get organization details | Read-Only | 100/min |
| PUT | /v1/organizations/{orgId} | Update organization config | Org Admin | 10/hour |
6.2.3 Template APIs
| Method | Endpoint | Description | Auth Role | Rate Limit |
|---|---|---|---|---|
| GET | /v1/organizations/{orgId}/templates | List templates (filter: category, status) | Read-Only | 100/min |
| GET | /v1/organizations/{orgId}/templates/{templateId} | Get template details | Read-Only | 100/min |
| GET | /v1/organizations/{orgId}/template-categories | List template categories | Read-Only | 100/min |
6.2.4 Schema Management APIs
| Method | Endpoint | Description | Auth Role | Rate Limit |
|---|---|---|---|---|
| GET | /v1/organizations/{orgId}/schemas | List supported vendor formats | Read-Only | 100/min |
| POST | /v1/organizations/{orgId}/schemas/validate | Pre-validate XML | Batch Operator | 20/hour |
6.2.5 Invoice APIs
| Method | Endpoint | Description | Auth Role | Rate Limit |
|---|---|---|---|---|
| GET | /v1/organizations/{orgId}/invoices/{invoiceId}/pdf | Download PDF | Read-Only | 1000/hour |
| GET | /v1/organizations/{orgId}/invoices/{invoiceId}/html | Download HTML | Read-Only | 1000/hour |
6.2.6 System APIs
| Method | Endpoint | Description | Auth Role | Rate Limit |
|---|---|---|---|---|
| GET | /v1/health | Health check | None | Unlimited |
| GET | /v1/version | API version info | None | Unlimited |
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|failedvendorCode: GASEL|XELLENT|ZYNERGYsearch: Batch name searchsortBy: uploadedAt|completedAt|batchNameorder: 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|failedpage: 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
| Field | Type | Min | Max | Format | Required | Validation |
|---|---|---|---|---|---|---|
| customerId | String | 1 | 50 | Alphanumeric, dash, underscore | Yes | `^[A-Za-z0-9_-]+ |
| personnummer | String | 10 | 13 | YYMMDD-XXXX or YYYYMMDD-XXXX | Conditional | Luhn algorithm |
| fullName | String | 1 | 255 | Unicode printable | Yes | Not empty, trim |
| String | 5 | 255 | RFC 5322 | No | Regex + optional DNS MX | |
| phone | String | 8 | 20 | E.164 recommended | No | `^+?[0-9\s-]+ |
| street | String | 1 | 255 | Any printable | Yes (postal) | Not empty |
| postalCode | String | 5 | 10 | Country-specific | Yes (postal) | Swedish: `^\d{3}\s?\d{2} |
| city | String | 1 | 100 | Any printable | Yes (postal) | Not empty |
| country | String | 2 | 2 | ISO 3166-1 alpha-2 | Yes | Enum: SE, NO, DK, FI |
7.1.2 Financial Data Validation
| Field | Type | Min | Max | Decimals | Required | Validation |
|---|---|---|---|---|---|---|
| subTotal | Decimal | 0.00 | 999999999.99 | 2 | Yes | ≥ 0 |
| taxAmount | Decimal | 0.00 | 999999999.99 | 2 | Yes | ≥ 0 |
| totalAmount | Decimal | 0.01 | 999999999.99 | 2 | Yes | > 0 |
| unitPrice | Decimal | 0.00 | 999999.99 | 2-6 | Yes | ≥ 0 |
| quantity | Decimal | 0.01 | 999999.99 | 2 | Yes | > 0 |
| taxRate | Decimal | 0 | 100 | 1 | Yes | Swedish: 0, 6, 12, 25 |
7.1.3 Business Logic Validation Rules
| Rule | Logic | Error Code | Message |
|---|---|---|---|
| Total Consistency | totalAmount == subTotal + taxAmount | AMOUNT_MISMATCH | Total must equal subtotal plus tax |
| Line Items Sum | sum(lineItems.lineAmount) == subTotal | LINE_ITEMS_MISMATCH | Line items must sum to subtotal |
| Date Logic | dueDate >= invoiceDate | INVALID_DATE_RANGE | Due date must be on or after invoice date |
| Tax Rate Valid | taxRate in [0, 6, 12, 25] | INVALID_TAX_RATE | Swedish VAT: 0%, 6%, 12%, or 25% |
| Currency Match | All amounts same currency | CURRENCY_MISMATCH | All amounts must use same currency |
| Personnummer Luhn | Luhn checksum | INVALID_PERSONNUMMER | Invalid Swedish personnummer |
| Swedish Postal Code | Format XXX XX | INVALID_POSTAL_CODE | Format 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:
- XML parser throws exception during parse attempt
- Catch exception, extract line/column from error
- Do NOT store file in blob storage
- Log error to Application Insights with file details (no content)
- 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:
- Store file in blob for manual review
- Update batch status to "failed"
- Log unsupported format to Application Insights
- Send alert to support team
- 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:
| Criterion | Validation Method | Target |
|---|---|---|
| Multi-region deployment operational | Verify services in both regions | Both regions active |
| Traffic Manager routes to healthy region | Simulate West Europe failure | Routes to North Europe |
| Database auto-failover tested | Simulate primary DB failure | Failover < 15 min |
| Blob geo-replication verified | Write to primary, read from secondary | Data replicated |
| Health checks on all services | GET /health on all endpoints | All return 200 |
| Automated incident alerts configured | Simulate service failure | Alert received within 5 min |
| Worker auto-restart on crash | Kill worker process | New instance starts |
| Queue message retry tested | Simulate worker crash mid-processing | Message reprocessed |
| Disaster recovery drill quarterly | Simulate complete region loss | Recovery within RTO |
| Backup restoration tested monthly | Restore database from backup | Successful restore |
Dependencies:
- Azure Traffic Manager configuration
- Multi-region resource deployment
- Database replication setup
- Automated failover testing procedures
- Incident response runbook
Risks & Mitigation (Nordic Context):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Both Azure regions fail simultaneously | VERY LOW | CRITICAL | - 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 regions | LOW | HIGH | - Each region operates independently - Eventual consistency acceptable - Manual reconciliation if partition >1 hour - Traffic Manager handles routing | Technical Architect |
| Database failover causes brief downtime | LOW | MEDIUM | - 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 connectivity | LOW | MEDIUM | - 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:
| Role | Scope | Permissions | Use Case |
|---|---|---|---|
| Super Admin | Global (all organizations) | Full CRUD on all resources, cross-org visibility | EG internal support team |
| Organization Admin | Single organization | Manage org users, configure settings, view all batches | Utility IT manager |
| Template Admin | Single organization | Create/edit templates, manage template versions | Utility design team |
| Batch Operator | Single organization | Upload batches, start processing, view status | Utility billing team |
| Read-Only User | Single organization | View batches, download invoices, view reports | Utility customer service |
| API Client | Single organization | Programmatic batch upload and status queries | Billing system integration |
Acceptance Criteria:
| Criterion | Validation Method | Target |
|---|---|---|
| OAuth 2.0 token required for all endpoints (except /health) | Call API without token | 401 Unauthorized |
| JWT token validated (signature, expiration, audience) | Tampered token, expired token | 401 Unauthorized |
| Refresh tokens work for 90 days | Use refresh token after 30 days | New access token issued |
| All 6 roles implemented in PostgreSQL | Query roles table | 6 roles present |
| Users can only access their organization | User A calls Org B endpoint | 403 Forbidden |
| All actions logged to audit_logs table | Perform action, query audit_logs | Entry created |
| API authentication middleware on all routes | Attempt bypass | All protected |
| MFA enforced for Super Admin | Login as Super Admin | MFA challenge |
| MFA enforced for Org Admin | Login as Org Admin | MFA challenge |
| Failed logins logged | 3 failed login attempts | 3 entries in audit_logs |
| Account lockout after 5 failed attempts | 6 failed login attempts | 15-minute lockout |
| API key rotation every 90 days | Check Key Vault secret age | Alert 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:
| Criterion | Validation Method | Target |
|---|---|---|
| All API traffic over HTTPS | Attempt HTTP request | Redirect to HTTPS or reject |
| TLS 1.3 or 1.2 enforced | Check TLS version in traffic | TLS ≥ 1.2 |
| Data encrypted at rest (blob) | Verify Azure encryption settings | Enabled |
| Data encrypted at rest (PostgreSQL) | Verify DB encryption | Enabled |
| Secrets in Azure Key Vault only | Code scan for hardcoded secrets | Zero secrets in code |
| No credentials in source control | Git history scan | Zero credentials |
| Database connections use managed identity | Check connection strings | No passwords |
| Personnummer not in URLs | URL pattern analysis | No personnummer patterns |
| Personnummer not in logs | Log analysis | No personnummer found |
4.4.3 Application Security (OWASP Top 10)
Security Measures:
| OWASP Risk | Mitigation | Validation |
|---|---|---|
| A01: Broken Access Control | Organization middleware, RBAC enforcement | Penetration testing |
| A02: Cryptographic Failures | TLS 1.3, AES-256, Key Vault | Security scan |
| A03: Injection | Parameterized queries, input validation | SQL injection testing |
| A04: Insecure Design | Threat modeling, security review | Architecture review |
| A05: Security Misconfiguration | Azure security baseline, CIS benchmarks | Configuration audit |
| A06: Vulnerable Components | Dependabot, automated scanning | Weekly scan |
| A07: Authentication Failures | OAuth 2.0, MFA, rate limiting | Penetration testing |
| A08: Software/Data Integrity | Code signing, SRI, checksums | Build verification |
| A09: Logging Failures | Comprehensive audit logging | Log completeness review |
| A10: SSRF | URL validation, allowlist | Security 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:
| Criterion | Validation Method | Target |
|---|---|---|
| Input validation on all API endpoints | Send malicious input | Rejected with error |
| SQL injection prevented | Attempt SQL injection in batch name | Sanitized/rejected |
| XSS prevented in templates | Inject script tags in template | Sanitized on render |
| XML external entity (XXE) attack prevented | Upload XXE payload | Parsing rejects |
| Billion laughs attack prevented | Upload billion laughs XML | Parsing rejects/times out safely |
| File upload size enforced | Upload 101MB file | Rejected at API gateway |
| Rate limiting prevents abuse | 1000 rapid API calls | 429 after limit |
| CSRF protection (future web UI) | Attempt CSRF attack | Blocked by token |
| Dependency vulnerabilities scanned weekly | Run Dependabot | Alerts for high/critical |
| Security headers present | Check HTTP response | X-Frame-Options, CSP, etc. |
4.4.4 Network Security
Acceptance Criteria:
| Criterion | Status | Phase |
|---|---|---|
| DDoS protection enabled (Azure basic) | ✅ Included | Phase 1 |
| IP whitelisting support for API clients | ✅ Optional feature | Phase 1 |
| VNet integration for Container Apps | ⚠️ Phase 2 | Phase 2 |
| Private endpoints for Blob Storage | ⚠️ Phase 2 | Phase 2 |
| Network Security Groups (NSGs) | ⚠️ Phase 2 | Phase 2 |
| Azure Firewall for egress filtering | ⚠️ Phase 2 | Phase 2 |
Dependencies:
- FluentValidation library
- OWASP dependency check tools
- Penetration testing (external vendor)
- Security code review process
Risks & Mitigation (Nordic/EU Security Context):
...
...
...