Document Information
| Field | Value |
|---|---|
| Project Name | EG Flow - Invoicing, Delivery, and Payment System |
| Document Type | Requirements Analysis Document |
| Version | 1.1 |
| Date | November 21, 2025 |
| Status | PENDING APPROVAL |
| Classification | Internal - Confidential |
Approval Required From:
| Stakeholder Role | Name | Signature | Date | Status |
|---|---|---|---|---|
| Product Owner | ☐ | |||
| Technical Architect | ☐ |
This Requirements Analysis Document serves as the single source of truth for EG Flow Phase 1 development. All requirements documented herein must be reviewed and approved by stakeholders before development begins.
Document Scope:
Out of Scope for Phase 1:
Documentation Standards:
┌─────────────────────────────────────────────────────────────────┐
│ 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) │
┌──────────────────────────────────────────────────────────────┐
│ 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} │ │ │
┌──────────────────────────────────────────────────────────────┐
│ 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)──┤ │
│ │ │
┌──────────────────────────────────────────────────────────────┐
│ 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)──┤ │
┌───────────────────────────────────────────────────────────┐
│ 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) │
┌──────────────────────────────────────────────────────────┐
│ 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 │
┌─────────────────────────────────────────────────────────┐
│ 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 │ │ │
┌──────────────────────────────────────────────────────────┐
│ 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
┌─────────────────────────────────────────────────────────┐
│ 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 ──┤ │
Standards:
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|[]
}
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| Method | Endpoint | Description | Auth Role | Rate Limit |
|---|---|---|---|---|
| GET | /v1/health | Health check | None | Unlimited |
| GET | /v1/version | API version info | None | Unlimited |
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}
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)" } }] }
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"
}
}
}
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
}
}
}
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
}
}
}
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"
}
}
}
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": []
}
}
| 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 |
| 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 |
| 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 |
Trigger: User uploads non-well-formed XML
System Action:
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"
}
}]
}
Trigger: XML namespace doesn't match GASEL, XELLENT, or ZYNERGY
System Action:
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"
}
}]
}
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)
Secondary Region: North Europe (Azure northeurope)
Traffic Routing:
**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:
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 |
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
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 |
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 |
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. |
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:
Risks & Mitigation (Nordic/EU Security Context):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| NIS2 Directive compliance (EU critical infrastructure) | MEDIUM | CRITICAL | - Energy sector falls under NIS2 - Incident reporting procedures (# EG Flow Phase 1 - Requirements Analysis Document |