EG Flow Phase 1 - Requirements Analysis Document
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 | ☐ |
Document Purpose
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:
- Business requirements with clear acceptance criteria and business value
- Functional requirements with complete technical specifications
- Non-functional requirements (performance, security, scalability, reliability)
- Complete data flow diagrams and processing pipelines
- Comprehensive error handling and validation rules
- Success criteria and validation methods
Out of Scope for Phase 1:
- 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
Table of Contents
- Project Overview
- Business Requirements
- Functional Requirements
- Non-Functional Requirements
- Data Flow Diagrams
- API Specifications
- Error Handling & Validation
- Glossary
- Appendices
2. Business Requirements
2.0 Priority Level Definitions
| Priority | Definition | Go-Live Impact | Stakeholder Approval | Example |
|---|---|---|---|---|
| CRITICAL | Core system functionality, data integrity, legal compliance, security | BLOCKING - Cannot go live without this | All stakeholders required | Authentication, GDPR, data isolation |
| HIGH | Primary business value, significant operational impact | BLOCKING - Should not go live without this | Product Owner + Technical Architect | Batch processing, multi-vendor XML |
| MEDIUM | Important for operations, workarounds exist temporarily | NON-BLOCKING - Can go live with plan to complete | Product Owner approval | Template management UI, postal integration |
| LOW | Nice to have, minimal business impact, future optimization | NON-BLOCKING - Defer to Phase 2 | Product Owner decision | Advanced filtering, custom dashboards |
2.1 BR-001: Multi-Tenant Organization Support
Requirement: The system shall support multiple independent utility companies (organizations) with complete data isolation at the blob storage, processing, and user access levels.
Business Value:
- Enables SaaS business model for Nordic utilities market
- Reduces per-customer infrastructure costs by 60-70%
- Accelerates new customer onboarding (weeks → days)
- Increases addressable market across Sweden, Norway, Denmark, Finland
Priority: CRITICAL
Nordic Market Context: Swedish utilities market has ~150 electricity suppliers, ~290 district heating companies. Multi-tenancy is essential for market penetration.
Acceptance Criteria:
| Criterion | Measurement Method | Test Case | Target |
|---|---|---|---|
| System supports 50+ concurrent organizations | Load test with 50 organizations uploading batches simultaneously | TC-001 | All batches process successfully |
| Each organization has isolated blob storage | Verify container naming: {org-id}-invoices-{year} | TC-002 | No cross-org container access |
| Users cannot access other organizations' data | Attempt API calls to different org-id with valid token | TC-003 | All return 403 Forbidden |
| Each organization configures own branding | Upload logo, colors; verify in rendered PDF | TC-004 | Branding applied correctly |
| Each organization defines delivery channels | Configure email priority; verify email sent first | TC-005 | Channel priority honored |
| Organization data never shared in logs | Review Application Insights logs for data leakage | TC-006 | No PII in logs |
Dependencies:
- Azure Blob Storage with container-per-organization strategy
- PostgreSQL schema with user_organization_roles table
- Middleware for organization context extraction and validation
- Role-based access control implementation
Risks & Mitigation (Nordic Context):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Data leakage between organizations | LOW | CRITICAL | - Middleware enforces org boundaries - Automated cross-org access testing - Penetration testing by external auditor - GDPR-compliant architecture review | Security Officer |
| Performance degradation with 50+ tenants | MEDIUM | HIGH | - Blob storage auto-scales - PostgreSQL connection pooling - Indexed queries on organization_id - Load testing with 100 organizations | Technical Architect |
| Swedish data residency requirements | LOW | HIGH | - West Europe primary region - Data residency enforced in org config - No cross-border data transfer | Compliance |
2.2 BR-002: Multi-Vendor XML Format Support
Requirement: The system shall process invoice batch files from multiple vendor billing systems (GASEL/Telinet, XELLENT/Karlskoga, ZYNERGY/EG Software) with automatic format detection and transformation to canonical JSON.
Business Value:
- Eliminates integration barrier for new customers (any billing system supported)
- Reduces onboarding time by 80% (no custom integration development)
- Increases market addressability to all Nordic utilities regardless of billing system
- Enables vendor-agnostic downstream processing
Priority: HIGH
Nordic Market Context: Top 3 billing systems in Swedish utilities market: Telinet (EDIEL format), Karlskoga/Xellent (OIOXML), EG Zynergy (proprietary). Supporting these covers ~70% of market.
Acceptance Criteria:
| Criterion | Measurement Method | Test Case | Target |
|---|---|---|---|
| System auto-detects GASEL format | Test with 50 GASEL XML samples | TC-010 | 100% accuracy |
| System auto-detects XELLENT format | Test with 50 XELLENT XML samples | TC-011 | 100% accuracy |
| System auto-detects ZYNERGY format | Test with 50 ZYNERGY XML samples | TC-012 | 100% accuracy |
| All formats transform to canonical JSON | Parse each format, verify JSON schema | TC-013 | All required fields present |
| XML validation against vendor XSD | Validate sample files against schemas | TC-014 | Schema validation passes |
| Clear error for unsupported formats | Upload unknown format XML | TC-015 | Returns 415 with vendor list |
| Detection within 1 second | Performance test with 100MB files | TC-016 | < 1 second |
Dependencies:
- Schema registry with field mappings for each vendor
- XML parsing library supporting complex namespaces
- XSD schema files from vendors (Telinet, Karlskoga, EG)
- Canonical JSON schema definition
Risks & Mitigation (Nordic Context):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Vendor XML schema changes without notice | MEDIUM | HIGH | - Version all schemas (v1.0, v1.1, etc.) - Support multiple versions simultaneously - 3-month deprecation notice in vendor contracts - Automated schema change detection - Direct vendor communication channels | Product Owner |
| EDIEL standard evolution | MEDIUM | MEDIUM | - Monitor Ediel.org for updates - Participate in Nordic Ediel working groups - Backward compatibility for 2 versions - Phased schema migration | Technical Architect |
| Complex namespace handling (OIOXML) | LOW | MEDIUM | - Use System.Xml.Linq for namespace support - XmlNamespaceManager for prefixes - Extensive unit testing per vendor - Schema mapping documentation | Dev Team |
| Incomplete field mappings | MEDIUM | MEDIUM | - Comprehensive mapping validation - Custom fields dictionary for unmapped data - Vendor format validation during onboarding - Optional field graceful degradation | Dev Team |
| Energiforsk format variations | LOW | LOW | - Document accepted variations - Lenient parsing mode option - Warning system for non-critical deviations | Product Owner |
2.3 BR-003: Batch Invoice Processing
Requirement: The system shall process batch invoice files containing up to 100,000 invoices with parallel processing, retry logic, and granular status tracking.
Business Value:
- Enables high-volume monthly invoice runs (typical Swedish utility: 50K-200K customers)
- Reduces manual processing effort by 95%
- Improves time-to-customer (days → hours)
- Enables predictable SLA commitments to customers
Priority: HIGH
Nordic Market Context: Monthly invoice cycles concentrated in first/last week of month. Heating season (Oct-Mar) has 40% higher volumes. System must handle seasonal peaks.
Acceptance Criteria:
| Criterion | Measurement Method | Test Case | Target |
|---|---|---|---|
| Single batch supports 100,000 invoices | Upload 100K batch, verify all processed | TC-020 | All invoices processed |
| Batches processed asynchronously | API returns 202 immediately, processing continues | TC-021 | < 500ms API response |
| Real-time progress tracking | Poll status API during processing | TC-022 | Updates every 30 seconds |
| Failed items don't block batch | Introduce 10 errors in 1000-item batch | TC-023 | 990 succeed, 10 fail independently |
| Retry mechanism (3 attempts) | Force temporary failure, verify retries | TC-024 | 3 retries then poison queue |
| Processing completes within 2 hours (100K) | Load test with 100K invoice batch | TC-025 | <= 120 minutes |
| Supports XML, JSON, CSV formats (Phase 1: XML only) | Upload each format type | TC-026 | XML fully supported |
Dependencies:
- Azure Storage Queues for message passing
- Worker auto-scaling (Container Apps with KEDA)
- Blob storage for batch source files and processed invoices
- Batch metadata storage with real-time updates
Risks & Mitigation (Nordic Context):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Processing timeout during heating season peaks (Oct-Mar) | MEDIUM | HIGH | - Pre-warm workers 1st/last week of month - Priority queue for SLA customers - Scheduled off-peak processing windows - Customer communication about peak times | Operations Manager |
| Memory constraints with large XML files (>50MB) | MEDIUM | MEDIUM | - Stream-based XML parsing (XmlReader) - Chunk processing for large batches - File size monitoring and alerts - 100MB hard limit with split guidance | Technical Architect |
| Disk space exhaustion on workers | LOW | MEDIUM | - Ephemeral storage cleanup after processing - Blob storage only for persistence - Worker instance monitoring | Operations Manager |
| Queue message 64KB limit exceeded | MEDIUM | MEDIUM | - Store invoice JSON in blob, queue has reference only - Message payload validation - Compression for large messages | Technical Architect |
2.4 BR-004: Template-Based Invoice Rendering
Requirement: The system shall generate PDF and HTML invoices using organization-specific Handlebars templates with dynamic data binding and brand customization.
Business Value:
- Enables brand consistency across all customer communications
- Supports regulatory requirements (Swedish Energy Markets Inspectorate guidelines)
- Allows per-organization customization without code changes
- Future-proofs for multi-language support (Swedish, Norwegian, Danish, Finnish)
Priority: HIGH
Nordic Market Context: Swedish regulations require specific invoice information (consumption details, tax breakdown, grid owner, metering point). Templates must support regulatory compliance.
Acceptance Criteria:
| Criterion | Measurement Method | Test Case | Target |
|---|---|---|---|
| Organizations upload custom Handlebars templates | Upload HTML template via API (future) or blob | TC-030 | Template stored successfully |
| Templates support dynamic data binding | Render with test invoice data | TC-031 | All fields populated |
| PDF generation from HTML with compliance | Generate PDF, verify format and content | TC-032 | A4, readable, complete |
| Template versioning supported | Create v2.0.0, verify old batches use v1.0.0 | TC-033 | Version isolation working |
| Templates updateable without affecting in-flight batches | Update template while batch processes | TC-034 | In-flight uses old version |
| Template validation before activation | Upload template with missing variable | TC-035 | Validation error returned |
| Organization branding applied | Logo, colors, fonts in rendered PDF | TC-036 | Branding visible |
| Swedish regulatory fields required | Verify mätpunkt, grid owner, consumption present | TC-037 | All required fields shown |
Dependencies:
- Handlebars.Net template engine library
- PDF generation library (Playwright with headless Chromium or IronPDF)
- Blob storage for template files
- Template metadata storage with version tracking
Risks & Mitigation (Nordic Context):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Template rendering performance bottleneck | HIGH | HIGH | - Compiled template caching (24-hour TTL) - Parallel rendering within 32-item batches - POC testing: render 10K invoices in < 5 min - Horizontal scaling of BatchItemWorkers | Technical Architect |
| PDF generation quality issues (Swedish characters) | MEDIUM | MEDIUM | - UTF-8 encoding throughout - Font embedding for åäö characters - Visual regression testing - Sample PDF review with Swedish content | QA Team |
| Swedish Energy Markets Inspectorate compliance | LOW | CRITICAL | - Legal review of template requirements - Required fields checklist in template validation - Compliance testing with regulatory samples - Annual regulatory update review | Legal/Compliance |
| Template injection attacks | LOW | CRITICAL | - Sandboxed Handlebars execution - No {{eval}} or {{exec}} helpers - Template sanitization - Security code review | Security Officer |
2.5 BR-005: Multi-Channel Delivery with Nordic Integration
Requirement: The system shall deliver invoices through multiple channels (email, postal, future: Kivra, e-Faktura, SMS) with configurable priority, automatic fallback, and integration with Nordic delivery partners.
Business Value:
- Increases delivery success rate to >98% (vs ~92% email-only)
- Reduces returned mail costs (postal fallback)
- Meets customer preferences (digital-first, postal backup)
- Enables compliance with Swedish "rätt till pappersfaktura" (right to paper invoice)
- Future-proofs for digital mailbox mandate discussions
Priority: HIGH
Nordic Market Context: Swedish law requires postal option for all customers. Kivra has 4.2M users in Sweden (40% population). E-faktura standard for B2B. Multi-channel is regulatory and competitive necessity.
Acceptance Criteria:
| Criterion | Measurement Method | Test Case | Target |
|---|---|---|---|
| Email delivery via SendGrid | Send 1000 test invoices | TC-040 | >95% delivered |
| Postal delivery via 21G bulk SFTP | Create zip, upload to 21G SFTP | TC-041 | File accepted by 21G |
| Channel priority configurable per org | Set priority [email, postal], verify order | TC-042 | Email attempted first |
| Automatic fallback on failure | Force email failure, verify postal attempted | TC-043 | Postal triggered automatically |
| Delivery status tracked per invoice | Check invoice metadata after delivery | TC-044 | Status and timestamps recorded |
| Retry logic for transient failures | Simulate SendGrid 429 rate limit | TC-045 | Retries with backoff |
| Delivery confirmation logged | Verify audit log entries | TC-046 | All deliveries logged |
| 21G bulk processing at scheduled times | Verify postal queue processed 12:00 and 20:00 | TC-047 | Batches sent on schedule |
Dependencies:
- SendGrid account with Nordic sender reputation
- 21G SFTP server access and credentials
- Azure Key Vault for credential storage
- Postal queue processing service (scheduled workers)
- ZIP file generation for 21G format
Risks & Mitigation (Nordic Context):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| SendGrid Nordic deliverability issues | MEDIUM | HIGH | - Dedicated IP for Nordic sending - SPF/DKIM/DMARC configuration - Sender reputation monitoring - Backup: Azure Communication Services - Gradual ramp-up for new organizations | Operations Manager |
| 21G SFTP connectivity issues | LOW | HIGH | - Retry logic with exponential backoff - Dual SFTP credentials (primary/backup) - Alert on connection failure - 21G SLA monitoring - Manual upload procedure documented | Operations Manager |
| Postal delivery delays (Swedish postal challenges) | MEDIUM | MEDIUM | - Set customer expectations (5-7 business days) - Track 21G processing confirmations - Escalation process for delays>10 days - Alternative print partner identified | Product Owner |
| Email spam filtering (Swedish ISPs) | MEDIUM | MEDIUM | - Warm up sending IP gradually - Monitor bounce rates by ISP - Whitelist requests to major ISPs (Telia, Tele2, Telenor) - Plain text + HTML multi-part emails | Operations Manager |
| Kivra API rate limits (future) | MEDIUM | MEDIUM | - Queue-based sending - Respect Kivra rate limits (documented in API) - Fallback to email if Kivra unavailable | Technical Architect |
2.6 BR-006: Real-Time Batch Status Visibility
Requirement: Users shall view batch processing status, progress statistics, and individual invoice statuses in real-time through the API with granular breakdown by processing stage and delivery channel.
Business Value:
- Reduces support inquiries by 60% (self-service status checking)
- Enables proactive issue resolution before customer complaints
- Improves operational transparency and trust
- Provides audit trail for service level agreement (SLA) tracking
Priority: MEDIUM
Nordic Market Context: Utility operations teams work standard hours (08:00-17:00). Real-time visibility enables remote monitoring and reduces after-hours escalations.
Acceptance Criteria:
| Criterion | Measurement Method | Test Case | Target |
|---|---|---|---|
| Batch status via API (queued/processing/completed/failed) | GET /batches/{id} during processing | TC-050 | Status reflects reality |
| Progress percentage accurate | Compare reported % to actual processed count | TC-051 | Within 1% accuracy |
| Individual invoice status queryable | GET /batches/{id}/items with status filter | TC-052 | Returns correct filtered list |
| Statistics include success/failure counts | Verify statistics.successfulItems matches reality | TC-053 | Exact match |
| Estimated completion time provided | Check estimatedCompletionAt during processing | TC-054 | Within 20% of actual |
| Failed invoices listed with error details | Query items with status=failed | TC-055 | Error messages present |
| Status updates within 30 seconds | Process batch, poll status API | TC-056 | Updates every ≤30 seconds |
| Delivery channel breakdown shown | Verify deliveryChannelBreakdown matches actual | TC-057 | Accurate breakdown |
Dependencies:
- Batch metadata updates from all workers
- Real-time statistics calculation (incremental updates)
- Blob storage access for metadata reads
- API caching strategy for frequently-polled batches
Risks & Mitigation:
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Performance impact of frequent metadata updates | MEDIUM | MEDIUM | - ETag-based optimistic concurrency - Batch statistics updates (not per-invoice) - Caching layer for status API - Throttle updates to max 1/minute | Technical Architect |
| Stale status data (eventual consistency) | MEDIUM | LOW | - Document 30-second update SLA - Timestamp on status response - Retry-After header for frequent polls - WebSocket push for Phase 2 | Product Owner |
2.7 BR-007: Security & Access Control
Requirement: The system shall implement OAuth 2.0 authentication with Microsoft Entra ID, role-based access control with 6 distinct roles, organization-level data isolation, and comprehensive audit logging compliant with Swedish and EU security standards.
Business Value:
- Ensures GDPR Article 32 compliance (security of processing)
- Prevents unauthorized data access and potential €20M GDPR fines
- Enables customer trust and enterprise sales (security requirements in tenders)
- Supports SOC 2 Type II compliance for future certification
Priority: CRITICAL
Nordic Market Context: Swedish utilities handle sensitive customer data (personnummer, consumption patterns). GDPR fines can reach 4% of annual turnover. Enterprise security is non-negotiable.
Acceptance Criteria:
| Criterion | Measurement Method | Test Case | Target |
|---|---|---|---|
| OAuth 2.0 with Entra ID authentication | Attempt API access with/without token | TC-060 | 401 without token, 200 with valid token |
| 6 roles implemented | Verify all roles in PostgreSQL | TC-061 | All 6 roles present |
| Users access only their organization data | User from Org A attempts Org B access | TC-062 | 403 Forbidden |
| All data access logged to audit trail | Review audit_logs table after operations | TC-063 | All actions logged |
| API authentication required (all endpoints except /health) | Call endpoints without token | TC-064 | 401 on all except /health |
| MFA enforced for admin roles | Admin login without MFA | TC-065 | MFA challenge presented |
| API key rotation every 90 days | Check key age in Key Vault | TC-066 | Alerts at 80 days |
| Failed login lockout (5 attempts = 15 min) | Attempt 6 failed logins | TC-067 | Account locked |
Dependencies:
- Microsoft Entra ID tenant configuration
- PostgreSQL schema for users, roles, user_organization_roles
- Azure Key Vault for secrets management
- Audit logging infrastructure
- MFA provider integration
Risks & Mitigation (Nordic/EU Context):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| GDPR non-compliance penalty | LOW | CRITICAL | - Annual GDPR compliance audit - Privacy Impact Assessment (PIA) completed - Data Protection Officer (DPO) review - Datainspektionen (Swedish DPA) guidelines followed - EU Standard Contractual Clauses for vendors | Legal/Compliance |
| Unauthorized personnummer access | LOW | CRITICAL | - Personnummer encrypted at rest - Access logging for all PII fields - Role-based field-level access control - Swedish Personal Data Act compliance | Security Officer |
| Authentication complexity delays onboarding | MEDIUM | MEDIUM | - Entra ID B2B invitation flow - Self-service org admin provisioning - Clear onboarding documentation - SSO with customer Entra ID tenants | Product Owner |
| Audit log storage costs (7-year retention) | MEDIUM | LOW | - PostgreSQL for hot logs (1 year) - Archive to blob after 1 year - Compressed JSON format - Query optimization | Technical Architect |
2.8 BR-008: GDPR & Swedish Data Protection Compliance
Requirement: The system shall comply with GDPR and Swedish Data Protection Law including all data subject rights, lawful processing basis, 7-year retention for invoices, data minimization, and Nordic data residency.
Business Value:
- Avoids GDPR fines (up to €20M or 4% of annual turnover)
- Enables enterprise sales (GDPR compliance is tender requirement)
- Builds customer trust and brand reputation
- Supports Swedish utility regulatory requirements (Energimarknadsinspektionen)
Priority: CRITICAL
Nordic Market Context: Swedish Datainspektionen actively enforces GDPR. Recent fines in telecom/energy sector for inadequate security. Data residency within EU required for many public utility contracts.
Acceptance Criteria:
| Criterion | Measurement Method | Test Case | Target |
|---|---|---|---|
| Right to access: Data export API | Request customer data export | TC-070 | JSON export with all customer invoices |
| Right to erasure: Anonymization | Request customer deletion, verify PII removed | TC-071 | Personnummer, name, email redacted |
| 7-year invoice retention | Check lifecycle policy config | TC-072 | Invoices retained 7 years |
| Audit logging for all data access | Query audit_logs for customer data access | TC-073 | All accesses logged |
| Data residency (Nordic countries only) | Verify blob storage regions | TC-074 | West/North Europe only |
| Consent management for digital delivery | Track customer delivery channel consent | TC-075 | Consent recorded |
| Privacy policy compliance | Legal review | TC-076 | Approved by legal |
| Lawful basis documented | Review data processing inventory | TC-077 | Contract basis for invoice data |
Dependencies:
- Legal review of GDPR compliance approach
- Data anonymization procedures and testing
- Azure Blob lifecycle policies for retention
- Data Processing Agreement (DPA) with Azure
- Privacy Impact Assessment (PIA) completion
Risks & Mitigation (Swedish/EU Context):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Datainspektionen (Swedish DPA) audit | LOW | CRITICAL | - Annual self-assessment against GDPR - Document all processing activities (ROPA) - DPO appointed and consulted - Privacy by design in architecture - Regular employee training | Legal/Compliance |
| Cross-border data transfer violations | LOW | CRITICAL | - All data stays in EU (Azure West/North Europe) - No backup to non-EU regions - Vendor contracts include EU Standard Clauses - Azure compliance certifications verified | Legal/Compliance |
| Personnummer handling violations | MEDIUM | CRITICAL | - Personnummer only in encrypted fields - Access logging for all reads - Minimal retention (7 years, then deletion) - Swedish Personal Data Act guidelines - No personnummer in URLs or logs | Security Officer |
| Right to erasure vs 7-year retention conflict | MEDIUM | HIGH | - Anonymization instead of deletion (legal basis: accounting law) - Legal opinion obtained - Balance data subject rights with legal obligations - Documented in privacy policy | Legal/Compliance |
| Third-party processor compliance (SendGrid, 21G) | MEDIUM | HIGH | - Data Processing Agreements (DPA) signed - Sub-processor list maintained - Regular compliance audits - EU-based data centers only | Legal/Compliance |
2.9 BR-009: Operational Monitoring & Observability
Requirement: The system shall provide comprehensive monitoring through Application Insights, structured logging with Serilog, real-time dashboards, and automated alerting to enable 24/7 operational visibility and proactive issue resolution.
Business Value:
- Reduces mean time to detection (MTTD) from hours to minutes
- Enables proactive issue resolution before customer impact
- Supports SLA compliance and reporting
- Reduces on-call burden through automated diagnostics
Priority: HIGH
Nordic Market Context: Swedish customers expect high service quality. Many utilities have SLA commitments (99.9% uptime). Winter heating season requires 24/7 monitoring.
Acceptance Criteria:
| Criterion | Measurement Method | Test Case | Target |
|---|---|---|---|
| Application Insights integrated (all services) | Verify telemetry in Azure portal | TC-080 | All services sending data |
| Structured logging with correlation IDs | Trace request across services | TC-081 | Same correlation ID |
| Custom metrics tracked | Verify metrics in dashboard | TC-082 | Batch, delivery, queue metrics present |
| Dashboards refresh every 5 minutes | Check dashboard timestamp | TC-083 | ≤ 5 minutes old |
| Dashboards accessible to authorized users | Login as different roles | TC-084 | Access granted appropriately |
| Critical alerts trigger within 5 minutes | Simulate high error rate | TC-085 | Alert received within 5 min |
| Alert escalation after 15 min unacknowledged | Don't acknowledge alert | TC-086 | Escalation triggered |
| PII masked in logs | Search logs for personnummer | TC-087 | No personnummer found |
Required Dashboards:
- Operations Dashboard: Active batches, queue depths, worker counts, error rate, health status
- Performance Dashboard: API latency, processing times, PDF generation, delivery latency
- Business Dashboard: Invoices processed, delivery breakdown, top organizations
- Vendor Dashboard: Batches by format, parsing success rates
Dependencies:
- Application Insights workspace
- Alert action groups (email, SMS)
- Dashboard configuration and permissions
- On-call rotation schedule
Risks & Mitigation (Nordic Context):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Alert fatigue from false positives | HIGH | MEDIUM | - Tuned thresholds based on baseline - 2-week monitoring before alerts live - Weekly alert review meetings - Alert suppression during maintenance | Operations Manager |
| Insufficient monitoring during off-hours | MEDIUM | HIGH | - 24/7 on-call rotation - Automated incident creation in Jira - Runbook for common issues - PagerDuty integration for escalation | Operations Manager |
| Log storage costs exceeding budget | LOW | MEDIUM | - 90-day log retention - Sampling for high-volume logs - Cost alerts at 80% budget - Archive old logs to blob | Technical Architect |
| Missing critical metrics | MEDIUM | MEDIUM | - Monthly metrics review - Stakeholder feedback on dashboard usefulness - Iterate dashboard based on incidents | Operations Manager |
2.10 BR-010: Test Data Strategy (GDPR Compliance)
Requirement: The system shall use only synthetic, non-sensitive test data in all non-production environments, with zero copying of production data to staging, to ensure GDPR compliance and prevent data breaches.
Business Value:
- Ensures GDPR Article 5(1)(b) compliance (purpose limitation)
- Prevents catastrophic data breach from non-production environments
- Reduces storage and transfer costs (no production data duplication)
- Enables offshore development team participation (India)
Priority: CRITICAL
Nordic Market Context: Swedish Datainspektionen prohibits production data in test environments. Indian development team cannot access personnummer data. European team must lead production debugging.
Acceptance Criteria:
| Criterion | Measurement Method | Test Case | Target |
|---|---|---|---|
| Zero production data in staging | Audit staging blob storage | TC-090 | No real personnummer found |
| Synthetic data generator creates realistic batches | Generate 10K synthetic invoices | TC-091 | Valid structure, fake PII |
| European team handles production issues | Production bug workflow documented | TC-092 | No PII sent to offshore team |
| Reproduction scenarios use synthetic data | Create synthetic scenario from production bug | TC-093 | Issue reproducible |
| Staging data clearly marked as test | Visual indicators in UI, filename patterns | TC-094 | Test data obvious |
| Production access restricted (European team only) | Attempt production access from India VPN | TC-095 | Access denied (geo-fenced) |
| Synthetic data follows Swedish patterns | Review generated personnummer, addresses | TC-096 | Realistic but invalid |
Dependencies:
- Synthetic data generation tool (Swedish personnummer, addresses, etc.)
- Staging environment completely isolated from production
- Network policies restricting production access
- Documentation of production issue workflow
Risks & Mitigation (EU/India Context):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Accidental production data leak to staging | LOW | CRITICAL | - Automated scanning for real personnummer patterns - No database copying tools - Production data access audit logs - Annual security training | Security Officer |
| Offshore team delays due to data restrictions | MEDIUM | MEDIUM | - European team creates reproduction scenarios - Comprehensive synthetic data sets - Well-documented issue reports - Pair programming for production issues | Product Owner |
| Synthetic data unrealistic (testing gaps) | MEDIUM | MEDIUM | - Generate from real data distributions (anonymized) - Edge cases library (long names, special chars) - Swedish address/personnummer validation - Regular synthetic data quality review | QA Team |
| Production debugging bottleneck (European team only) | MEDIUM | MEDIUM | - European team on-call 24/7 - Comprehensive monitoring reduces debugging needs - Runbooks for common issues - Knowledge transfer to European team | Operations Manager |
2.11 BR-011: Invoice Download API
Requirement: Users shall be able to download generated invoices (PDF and HTML) through authenticated API endpoints with access control and audit logging.
Business Value:
- Enables customer service to retrieve invoices for disputes
- Supports reprint requests without reprocessing
- Facilitates regulatory compliance (invoice provision upon request)
- Enables future customer portal integration
Priority: HIGH
Acceptance Criteria:
| Criterion | Measurement Method | Test Case | Target |
|---|---|---|---|
| API endpoint to download PDF by invoice ID | GET /invoices/{id}/pdf | TC-100 | PDF file returned |
| API endpoint to download HTML by invoice ID | GET /invoices/{id}/html | TC-101 | HTML returned |
| Download links in batch item response | GET /batches/{id}/items/{itemId} | TC-102 | pdfUrl and htmlUrl present |
| Access control: org users only | User from Org A downloads Org B invoice | TC-103 | 403 Forbidden |
| Download generates audit log entry | Download invoice, check audit_logs | TC-104 | Entry created |
| Content-Disposition header for PDF | Check HTTP response headers | TC-105 | Filename in header |
| Rate limiting on downloads | Download 1000 invoices rapidly | TC-106 | Rate limit applied |
Dependencies:
- Blob storage SAS token generation
- Access control middleware
- Audit logging service
Risks & Mitigation:
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Unauthorized invoice access | LOW | HIGH | - Organization boundary checks - Audit all downloads - Rate limiting - Anomaly detection | Security Officer |
| Bandwidth costs for large volumes | MEDIUM | LOW | - CDN for frequently accessed invoices - Compression - Monitoring | Technical Architect |
2.12 BR-012: Batch History & Search
Requirement: Users shall be able to search, filter, and list historical batches for their organization with support for date ranges, status filters, and vendor format filtering.
Business Value:
- Enables troubleshooting and root cause analysis
- Supports operational reporting and SLA tracking
- Facilitates audit compliance
- Improves user experience with search capabilities
Priority: MEDIUM
Acceptance Criteria:
| Criterion | Measurement Method | Test Case | Target |
|---|---|---|---|
| List batches with pagination | GET /batches?page=1&pageSize=50 | TC-110 | Paginated list returned |
| Filter by date range | GET /batches?from=2025-11-01&to=2025-11-30 | TC-111 | Only Nov batches returned |
| Filter by status | GET /batches?status=completed | TC-112 | Only completed batches |
| Filter by vendor format | GET /batches?vendorCode=GASEL | TC-113 | Only GASEL batches |
| Search by batch name | GET /batches?search=November | TC-114 | Name contains "November" |
| Sort by upload/completion date | GET /batches?sortBy=uploadedAt&order=desc | TC-115 | Newest first |
| Returns last 90 days by default | GET /batches (no filters) | TC-116 | Last 90 days only |
| Access restricted to organization | User queries batches from other org | TC-117 | Empty results |
Dependencies:
- Batch metadata indexing strategy
- Query performance optimization
- Blob storage metadata queries or search index
Risks & Mitigation:
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Slow queries with large batch history | MEDIUM | MEDIUM | - Index on organization_id, date - Pagination required - Consider Azure Cognitive Search for Phase 2 | Technical Architect |
2.13 BR-013: Notification System
Requirement: The system shall send email notifications to designated organization contacts when batch processing completes successfully or fails, including summary statistics and error reports.
Business Value:
- Reduces manual status checking effort by 80%
- Enables proactive issue resolution
- Improves customer satisfaction with timely updates
- Supports SLA monitoring and reporting
Priority: MEDIUM
Acceptance Criteria:
| Criterion | Measurement Method | Test Case | Target |
|---|---|---|---|
| Email sent on batch completion | Complete batch, verify email received | TC-120 | Email received within 5 min |
| Email sent on batch failure | Force batch failure, verify email | TC-121 | Email received within 5 min |
| Email includes summary statistics | Review email content | TC-122 | Total, success, failed counts present |
| Email includes error report for failures | Review failure email | TC-123 | Failed items listed with reasons |
| Recipients configurable per organization | Update org config, verify new recipient | TC-124 | Email to correct recipients |
| Email template professional and branded | Review email design | TC-125 | EG branding applied |
| Links to batch status in email | Click link in email | TC-126 | Opens to batch detail |
Dependencies:
- Email service (SendGrid or Azure Communication Services)
- Email templates (HTML)
- Organization configuration for recipients
Risks & Mitigation:
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Email deliverability to org admins | MEDIUM | MEDIUM | - SPF/DKIM/DMARC for notification domain - Fallback to SMS for critical alerts | Operations Manager |
| Notification fatigue | MEDIUM | LOW | - Configurable notification thresholds - Digest emails for multiple batches - Opt-in for verbose notifications | Product Owner |
3. Functional Requirements
3.1 FR-001: Batch File Upload
Requirement: Users shall upload batch invoice files through a RESTful API with automatic vendor format detection, file validation, and storage in organization-specific blob containers with month/day hierarchy.
Priority: HIGH
API Endpoint: POST /organizations/{organizationId}/batches
Request Format:
POST /v1/organizations/123e4567-e89b-12d3-a456-426614174000/batches HTTP/1.1 Host: api.egflow.com Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbG... Content-Type: multipart/form-data; boundary=----WebKitFormBoundary ------WebKitFormBoundary Content-Disposition: form-data; name="file"; filename="invoices_nov.xml" Content-Type: application/xml <?xml version="1.0" encoding="UTF-8"?> <InvoiceBatch xmlns="urn:ediel:se:electricity:invoice:1.0"> ... </InvoiceBatch> ------WebKitFormBoundary Content-Disposition: form-data; name="metadata" Content-Type: application/json { "batchName": "Invoice_November_2025", "priority": "normal" } ------WebKitFormBoundary--
Response Format (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:a3d5e7f9b2c4d6e8...",
"detectedFormat": "GASEL"
},
"blobPath": "acme-batches-2025/11/21/550e8400.../source.xml"
},
"metadata": {
"timestamp": "2025-11-21T10:30:00Z",
"requestId": "req-abc-123",
"apiVersion": "1.0"
}
}
Blob Storage Path Pattern (Updated per Nov 21 decision):
{org-id}-batches-{year}/{month}/{day}/{batch-id}/source.xml
Acceptance Criteria:
| Criterion | Validation Method | Test Data | Expected Result |
|---|---|---|---|
| Accepts XML files up to 100MB | Upload 100MB XML file | gasel_100mb.xml | 201 Created |
| Validates file is well-formed XML | Upload malformed XML | invalid.xml | 400 INVALID_XML |
| Stores in org-specific container with month/day path | Upload, verify blob path | valid.xml | Path: {org}/2025/11/21/{id}/source.xml |
| Returns unique UUID batch ID | Upload, check batchId format | valid.xml | Valid UUID v4 |
| Detects GASEL format | Upload GASEL XML | gasel_sample.xml | detectedFormat: "GASEL" |
| Detects XELLENT format | Upload XELLENT XML | xellent_sample.xml | detectedFormat: "XELLENT" |
| Detects ZYNERGY format | Upload ZYNERGY XML | zynergy_sample.xml | detectedFormat: "ZYNERGY" |
| Calculates SHA-256 checksum | Upload, verify checksum | valid.xml | Checksum matches file |
| Requires Batch Operator role | Upload without role | valid.xml | 403 ACCESS_DENIED |
| Rate limited (10 uploads/hour/org) | Upload 11 files in 1 hour | valid.xml x11 | 11th returns 429 |
| File size > 100MB rejected | Upload 101MB file | large.xml | 413 FILE_TOO_LARGE |
| Non-XML files rejected | Upload PDF file | invoice.pdf | 415 UNSUPPORTED_FORMAT |
Validation Rules:
| Field | Rule | Error Code | Error Message |
|---|---|---|---|
| file | Required | VALIDATION_ERROR | File is required |
| file.size | 1KB ≤ size ≤ 100MB | FILE_TOO_LARGE | File must be between 1KB and 100MB |
| file.contentType | Must be application/xml or text/xml | INVALID_CONTENT_TYPE | File must be XML format |
| file.content | Well-formed XML (parseable) | INVALID_XML | XML file is not well-formed. Line {line}, Column {column}: {error} |
| metadata.batchName | 1-255 characters, no path separators | VALIDATION_ERROR | Batch name must be 1-255 characters without / or \ |
| metadata.priority | Must be "normal" or "high" | VALIDATION_ERROR | Priority must be 'normal' or 'high' |
Error Scenarios:
| Scenario | HTTP | Error Code | Message | Details |
|---|---|---|---|---|
| File too large | 413 | FILE_TOO_LARGE | File exceeds 100MB limit | { "fileSize": 105906176, "limit": 104857600 } |
| Invalid XML | 400 | INVALID_XML | XML file is not well-formed | { "line": 142, "column": 23, "error": "Unexpected end tag" } |
| Missing token | 401 | UNAUTHORIZED | Missing or invalid authentication token | { "suggestion": "Include Authorization: Bearer {token} header" } |
| Insufficient permissions | 403 | ACCESS_DENIED | User does not have Batch Operator role | { "requiredRole": "Batch Operator", "userRoles": ["Read-Only"] } |
| Rate limit exceeded | 429 | RATE_LIMIT_EXCEEDED | Too many batch uploads. Limit: 10 per hour | { "limit": 10, "window": "1 hour", "retryAfter": "2025-11-21T11:30:00Z" } |
3.2 FR-002: Batch Processing Initiation
Requirement: Users shall initiate batch processing through API, which enqueues the batch to batch-upload-queue for asynchronous processing by ParserService.
Priority: HIGH
API Endpoint: POST /organizations/{organizationId}/batches/{batchId}/start
Request Format:
POST /v1/organizations/{orgId}/batches/{batchId}/start HTTP/1.1 Authorization: Bearer {token} Content-Type: application/json { "validationMode": "strict" }
Response Format (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,
"queueName": "batch-upload-queue"
}
}
Processing Flow:
1. Validate batch exists and status is "uploaded"
2. Create message in batch-upload-queue with batch metadata
3. Update batch status to "queued" in blob metadata
4. Return 202 Accepted with queue position
5. ParserService picks up message asynchronously
Acceptance Criteria:
| Criterion | Validation Method | Test Data | Expected Result |
|---|---|---|---|
| Batch must be in "uploaded" status | Start already-processing batch | processing-batch-id | 409 CONFLICT |
| Creates message in batch-upload-queue | Verify queue message created | valid-batch-id | Message present |
| Updates batch status to "queued" | Check batch metadata after start | valid-batch-id | status: "queued" |
| Returns estimated time based on queue | Check with empty vs full queue | various | Time varies with queue depth |
| Idempotent (duplicate calls safe) | Call /start twice | valid-batch-id | Both return 202, one processes |
| Returns current queue position | Verify position accuracy | valid-batch-id | Position matches queue |
| Requires Batch Operator role | Call without role | valid-batch-id | 403 ACCESS_DENIED |
| Supports "strict" and "lenient" validation | Set validationMode=lenient | valid-batch-id | Mode stored in message |
| Queue full returns 503 | Start when queue depth >10000 | valid-batch-id | 503 SERVICE_UNAVAILABLE |
Validation Rules:
| Check | Rule | Error Code | HTTP | Action |
|---|---|---|---|---|
| Batch exists | Must exist in blob storage | RESOURCE_NOT_FOUND | 404 | Return error immediately |
| Batch ownership | Must belong to organization in path | ACCESS_DENIED | 403 | Return error immediately |
| Batch status | Must be "uploaded", not "queued"/"processing"/"completed" | PROCESSING_ERROR | 422 | Return error with current status |
| Organization active | Organization.isActive = true | ORGANIZATION_INACTIVE | 422 | Return error |
| Queue capacity | Queue depth < 10,000 | SERVICE_UNAVAILABLE | 503 | Return with retry-after |
| User permission | User has BatchOperator or higher role | ACCESS_DENIED | 403 | Return error |
| Validation mode | Must be "strict" or "lenient" | VALIDATION_ERROR | 400 | Return error with allowed values |
Error Scenarios:
| Scenario | HTTP | Error Code | Message | Details | User Action |
|---|---|---|---|---|---|
| Batch not found | 404 | RESOURCE_NOT_FOUND | Batch does not exist | { "batchId": "{id}" } | Verify batch ID |
| Already processing | 409 | CONFLICT | Batch is already processing | { "currentStatus": "processing", "startedAt": "2025-11-21T10:00:00Z" } | Wait for completion or cancel |
| Invalid status | 422 | PROCESSING_ERROR | Batch cannot be started from current status | { "currentStatus": "completed", "allowedStatuses": ["uploaded"] } | Re-upload batch |
| Queue at capacity | 503 | SERVICE_UNAVAILABLE | System at capacity, retry later | { "queueDepth": 10500, "retryAfter": "2025-11-21T11:00:00Z" } | Schedule for off-peak |
3.3 FR-003: Parser Service (XML → JSON Transformation)
Requirement: The ParserService shall listen to batch-upload-queue, download batch XML from blob storage, detect vendor format, validate against XSD schema, parse to individual invoices, transform to canonical JSON format, and enqueue to batch-items-queue in groups of 32.
Priority: CRITICAL
Service Specifications:
Trigger: Message in batch-upload-queue
Input: Batch ID from queue message
Output: Individual JSON files in blob + messages in batch-items-queue
Processing Steps:
1. Dequeue message from batch-upload-queue
2. Download batch XML from blob: {org}-batches-{year}/{month}/{day}/{batch-id}/source.xml
3. Detect vendor format (namespace + structure analysis):
- urn:ediel → GASEL
- oio.dk → XELLENT
- Zynergy → ZYNERGY
4. Load vendor-specific schema mapping from: {org}-data/schemas/{vendor}-mapping.json
5. Validate XML against vendor XSD schema
6. Parse XML using XPath expressions from mapping
7. Transform each invoice to canonical JSON format
8. Store individual JSON files: {org}-invoices-{year}/{month}/{day}/{invoice-id}.json
9. Group invoices into batches of 32
10. Enqueue each 32-item batch to batch-items-queue
11. Update batch metadata: totalItems, vendor info, status="processing"
12. Delete message from batch-upload-queue (on success)
13. On error: Retry (3x with backoff) or move to poison queue
Canonical JSON Schema (Output Format):
{
"invoiceId": "uuid",
"invoiceNumber": "2025-11-001",
"invoiceDate": "2025-11-06",
"dueDate": "2025-11-20",
"currency": "SEK",
"periodStart": "2025-10-01",
"periodEnd": "2025-10-31",
"customer": {
"customerId": "020624-2380",
"fullName": "Medeni Schröder",
"firstName": null,
"lastName": null,
"email": "muntaser.af@zavann.net",
"phone": "09193538799",
"address": {
"street": "Strandbo 63B",
"houseNumber": null,
"apartment": null,
"city": "Växjö",
"postalCode": "352 58",
"country": "SE"
},
"taxIdentifier": "020624-2380",
"customerType": "private"
},
"invoiceDetails": {
"subTotal": 599.42,
"taxAmount": 149.86,
"totalAmount": 749.28,
"lineItems": [
{
"lineNumber": 1,
"description": "Elförbrukning - Fast pris",
"quantity": 420,
"unit": "KWH",
"unitPrice": 1.026,
"lineAmount": 430.92,
"taxRate": 25.0,
"taxAmount": 107.73,
"category": "electricity"
},
{
"lineNumber": 2,
"description": "Månadsavgift",
"quantity": 1,
"unit": "MON",
"unitPrice": 15.20,
"lineAmount": 15.20,
"taxRate": 25.0,
"taxAmount": 3.80,
"category": "fee"
}
]
},
"delivery": {
"meteringPointId": "735999756427205424",
"gridArea": "SE4",
"gridOwner": "Växjö Energi Elnät AB",
"previousReading": {
"date": "2025-09-30",
"value": 10580,
"type": "Actual"
},
"currentReading": {
"date": "2025-10-31",
"value": 11000,
"type": "Actual"
},
"consumption": 420,
"consumptionUnit": "kWh"
},
"payment": {
"paymentId": "202511001",
"paymentMethod": "Bankgiro",
"bankAccount": "168-6039",
"dueDate": "2025-11-20"
},
"sourceMetadata": {
"vendorCode": "GASEL",
"vendorVersion": "1.0",
"originalBatchId": "BATCH2025110600001",
"originalInvoiceId": "2025-11-001",
"contractReference": "CON-2024-001",
"customFields": {
"productCode": "telinet_fixed",
"productName": "Fast Pris Vintersäkra",
"taxClassification": "Normal"
},
"parsedAt": "2025-11-21T10:35:45Z"
}
}
Acceptance Criteria:
| Criterion | Validation Method | Test Data | Expected Result |
|---|---|---|---|
| Listens to batch-upload-queue | Send message, verify service picks up | Queue message | Message dequeued within 30s |
| Downloads batch XML from blob | Verify file download logged | Batch in blob | File downloaded successfully |
| Detects GASEL format (100% accuracy) | Test with 50 GASEL samples | gasel_*.xml | All detected as GASEL |
| Detects XELLENT format (100% accuracy) | Test with 50 XELLENT samples | xellent_*.xml | All detected as XELLENT |
| Detects ZYNERGY format (100% accuracy) | Test with 50 ZYNERGY samples | zynergy_*.xml | All detected as ZYNERGY |
| Loads correct schema mapping | Verify mapping file loaded | gasel_sample.xml | gasel-mapping.json loaded |
| Validates XML against XSD | Upload invalid GASEL XML | invalid_gasel.xml | Validation errors in batch metadata |
| Parses GASEL using XPath mappings | Parse GASEL, verify JSON fields | gasel_sample.xml | All fields extracted |
| Parses XELLENT with namespace handling | Parse XELLENT (com:, main: prefixes) | xellent_sample.xml | All fields extracted |
| Parses ZYNERGY nested structure | Parse ZYNERGY | zynergy_sample.xml | All fields extracted |
| Transforms to canonical JSON | Verify JSON schema compliance | All vendors | All pass schema validation |
| Stores JSON: {org}-invoices-{year}/{month}/{day}/{id}.json | Check blob path | Parsed invoice | Correct path used |
| Groups into 32-item batches | Parse 100 invoices, count queue messages | 100-invoice batch | 4 messages (32+32+32+4) |
| Enqueues to batch-items-queue | Verify messages in queue | Parsed batch | Messages present |
| Updates batch metadata | Check metadata after parsing | Parsed batch | totalItems, vendorCode set |
| Deletes from batch-upload-queue on success | Verify message removed | Successful parse | Message gone |
| Retries 3x on transient errors | Force blob download error | Failing batch | 3 retries logged |
| Moves to poison queue after 3 failures | Force permanent error | Failing batch | Message in poison queue |
| Parsing completes within 2 minutes for 10K batch | Performance test | 10K-invoice XML | ≤ 120 seconds |
Vendor-Specific Parsing Rules:
GASEL Format:
Required Elements (validation fails if missing):
- BatchHeader/InterchangeID
- BatchHeader/TotalInvoiceCount
- SupplierParty/PartyName
- Invoice/InvoiceHeader/InvoiceNumber
- Invoice/CustomerParty/PartyName
- Invoice/MonetarySummary/PayableAmount
Optional Elements (null if missing):
- Invoice/DeliveryLocation/MeteringPointID
- Invoice/ContractDetails/ContractID
- Invoice/CustomerParty/Contact/ElectronicMail
- Invoice/CustomerParty/Contact/Telephone
Date Format: ISO 8601 (YYYY-MM-DD)
Amount Format: Decimal with 2 places, period as separator
Namespace: urn:ediel:se:electricity:invoice:1.0
XELLENT Format:
Required Elements:
- BatchHeader/BatchID
- com:ID (invoice number)
- com:IssueDate
- com:BuyerParty/com:PartyName/com:Name
- com:LegalTotals/com:ToBePaidTotalAmount
Optional Elements:
- com:BuyerParty/com:ContactInformation/@E-Mail
- com:BuyerParty/com:Address
Special Handling:
- Multiple namespaces (com:, main:, fsv:)
- Amount format: "1 245,00" (space separator, comma decimal)
- Must normalize to standard decimal format
Namespace: http://rep.oio.dk/ubl/xml/schemas/0p71/pie/
ZYNERGY Format:
Required Elements:
- BatchHeader/BatchId
- InvoiceData/InvoiceNumber
- Customer/ReadOnlyFullName
- InvoiceData/InvoiceAmount
Optional Elements:
- Customer/FirstName and LastName (if ReadOnlyFullName empty)
- InvoiceAddress/EmailAddress
- VAT details
Special Handling:
- Nested structure (Invoice > Customer, InvoiceData, InvoiceAddress, VAT)
- Multiple company references (CompaniesId throughout)
- InvoiceAmount vs InvoiceBalance distinction
Namespace: http://eg.dk/Zynergy/1.0/invoice.xsd
Dependencies:
- Azure Storage Queue:
batch-upload-queue - Vendor schema mappings in blob storage
- XSD schema files for validation
- Canonical JSON schema definition
- Error handling and retry infrastructure
Risks & Mitigation:
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Large XML file memory issues (>50MB) | MEDIUM | HIGH | - Stream-based parsing (XmlReader, not XDocument) - Process invoices incrementally - Worker memory limit monitoring - File size alerts at 75MB | Technical Architect |
| Parsing performance bottleneck | MEDIUM | HIGH | - Parallel XPath evaluation where possible - Compiled XPath expressions cached - POC: parse 10K invoices in <2 minutes - Horizontal scaling of ParserService | Technical Architect |
| XSD validation performance | LOW | MEDIUM | - Cache compiled XSD schemas - Make validation optional in lenient mode - Async validation (don't block parsing) | Technical Architect |
| Vendor-specific edge cases | HIGH | MEDIUM | - Extensive test suite per vendor (50+ samples) - Error collection from production - Vendor liaison for unclear cases - Lenient mode for known variations | Product Owner |
3.4 FR-004: Document Generator Service (JSON → HTML → PDF)
Requirement: The DocumentGeneratorService (based on existing zyn-DocumentGenerator) shall listen to batch-items-queue, load Handlebars templates, render HTML with invoice data, generate PDFs using Playwright, and store documents in blob storage with month/day hierarchy.
Priority: CRITICAL
Service Specifications:
Trigger: Message in batch-items-queue (contains 32 invoice references)
Input: Batch of 32 invoice IDs and organization ID
Output: PDF + HTML files in blob storage, messages in distribution routing queues
Processing Steps (per 32-item batch):
1. Dequeue message from batch-items-queue
2. Acquire blob lease: {org}-batches-{year}/{month}/{day}/{batch-id}/locks/{worker-id}.lock
3. For each of 32 invoices:
a. Download JSON: {org}-invoices-{year}/{month}/{day}/{invoice-id}.json
b. Load organization config: {org}-data/organization.json
c. Determine template category from distribution type
d. Load Handlebars template: {org}-data/templates/{category}/active.html
e. Load organization branding (logo from blob URL, colors, fonts)
f. Compile Handlebars template (cache compiled version)
g. Render HTML with invoice data + branding
h. Generate PDF from HTML using Playwright (headless Chromium)
i. Store HTML: {org}-invoices-{year}/{month}/{day}/{invoice-id}.html
j. Store PDF: {org}-invoices-{year}/{month}/{day}/{invoice-id}.pdf
k. Update invoice JSON metadata with file paths and render timestamp
l. Determine distribution method from invoice data
m. Enqueue to appropriate queue:
- Mail (postal) → postal-bulk-queue (processed in bulk)
- Email → email-queue
- SMS (future) → sms-queue
- Kivra (future) → kivra-queue
- E-faktura (future) → efaktura-queue
4. Release blob lease
5. Update batch metadata: processedItems += 32
6. Delete message from batch-items-queue
7. On error: Retry (3x) or poison queue
Acceptance Criteria:
| Criterion | Validation Method | Test Data | Expected Result |
|---|---|---|---|
| Listens to batch-items-queue | Send message, verify pickup | Queue message | Dequeued within 30s |
| Processes 32 invoices per message | Send 32-item batch, count outputs | 32 invoices | 32 PDFs generated |
| Acquires blob lease before processing | Check lease on blob | Valid batch | Lease acquired |
| Downloads invoice JSON from correct path | Verify download logged | Invoice JSON | Correct path: {org}/2025/11/21/{id}.json |
| Loads organization template | Verify template file accessed | Org with template | Template loaded |
| Determines template category correctly | Invoice → "invoice" template, Letter → "confirmation" | Various types | Correct template used |
| Compiles Handlebars template | Render with variables | Template with {{invoiceNumber}} | Number inserted |
| Caches compiled templates (24h) | Render same template twice | Same template | Second render faster |
| Renders HTML with Swedish characters | Render with åäö | Swedish invoice | Characters correct |
| Generates PDF with Playwright | Convert HTML to PDF | Rendered HTML | PDF created, A4 format |
| PDF includes organization branding | Check PDF for logo, colors | Branded template | Branding visible |
| Stores HTML in correct blob path | Verify path | Generated HTML | {org}/invoices/2025/11/21/{id}.html |
| Stores PDF in correct blob path | Verify path | Generated PDF | {org}/invoices/2025/11/21/{id}.pdf |
| Updates invoice metadata JSON | Check metadata after render | Processed invoice | fileReferences populated |
| Determines distribution method | Check routing logic | Various configs | Correct queue selected |
| Enqueues to postal-bulk-queue for mail | Invoice with postal delivery | Mail invoice | Message in postal queue |
| Enqueues to email-queue for email | Invoice with email delivery | Email invoice | Message in email queue |
| Releases blob lease on completion | Verify lease released | Processed batch | Lease gone |
| Updates batch statistics | Check batch metadata | Processed batch | processedItems incremented |
| Rendering within 2 seconds per invoice (p95) | Performance test 1000 invoices | Various | p95 ≤ 2 seconds |
| PDF generation within 5 seconds per invoice (p95) | Performance test 1000 PDFs | Various | p95 ≤ 5 seconds |
| Retries on transient errors | Force blob error | Failing invoice | 3 retries attempted |
| Moves to poison queue after 3 failures | Force permanent error | Failing invoice | Poison queue message |
Handlebars Template Example:
<!DOCTYPE html> <html lang="sv"> <head> <meta charset="UTF-8"> <title>Faktura {{invoiceNumber}}</title> <style> body { font-family: {{organization.fontFamily}}; } .header { background-color: {{organization.primaryColor}}; color: white; padding: 20px; } .logo { max-width: 200px; } </style> </head> <body> <div class="header"> <img src="{{organization.logoUrl}}" alt="{{organization.displayName}}" class="logo" /> <h1>Faktura {{invoiceNumber}}</h1> </div> <div class="customer-info"> <h2>Kund</h2> <p><strong>{{customer.fullName}}</strong></p> <p>{{customer.address.street}}</p> <p>{{customer.address.postalCode}} {{customer.address.city}}</p> </div> <div class="invoice-details"> <p><strong>Fakturadatum:</strong> {{invoiceDate}}</p> <p><strong>Förfallodatum:</strong> {{dueDate}}</p> <p><strong>Period:</strong> {{periodStart}} - {{periodEnd}}</p> {{#if delivery.meteringPointId}} <p><strong>Mätpunkt:</strong> {{delivery.meteringPointId}}</p> <p><strong>Elområde:</strong> {{delivery.gridArea}}</p> <p><strong>Förbrukning:</strong> {{delivery.consumption}} {{delivery.consumptionUnit}}</p> {{/if}} </div> <table> <thead> <tr> <th>Beskrivning</th> <th>Antal</th> <th>Enhet</th> <th>Pris</th> <th>Belopp</th> </tr> </thead> <tbody> {{#each invoiceDetails.lineItems}} <tr> <td>{{this.description}}</td> <td>{{formatNumber this.quantity decimals=2}}</td> <td>{{this.unit}}</td> <td>{{formatCurrency this.unitPrice}}</td> <td>{{formatCurrency this.lineAmount}}</td> </tr> {{/each}} <tr class="total-row"> <td colspan="4"><strong>Delsumma:</strong></td> <td><strong>{{formatCurrency invoiceDetails.subTotal}}</strong></td> </tr> <tr> <td colspan="4"><strong>Moms (25%):</strong></td> <td><strong>{{formatCurrency invoiceDetails.taxAmount}}</strong></td> </tr> <tr class="total-row"> <td colspan="4"><strong>Att betala:</strong></td> <td><strong>{{formatCurrency invoiceDetails.totalAmount}} {{currency}}</strong></td> </tr> </tbody> </table> {{#if payment.paymentId}} <div class="payment-info"> <h3>Betalningsinformation</h3> <p><strong>OCR-nummer:</strong> {{payment.paymentId}}</p> <p><strong>Bankgiro:</strong> {{payment.bankAccount}}</p> <p><strong>Förfallodatum:</strong> {{payment.dueDate}}</p> </div> {{/if}} </body> </html>
Dependencies:
- Handlebars.Net library for template rendering
- Playwright library for PDF generation
- Azure Storage Queue:
batch-items-queue - Blob storage for templates, JSON, PDF, HTML
- Organization configuration in blob
- Custom Handlebars helpers (formatCurrency, formatNumber, formatDate)
Risks & Mitigation:
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Handlebars rendering performance | HIGH | HIGH | - Pre-compile templates on first use - Cache compiled templates (24h TTL) - Parallel rendering for 32 items - POC: 1000 renders in <30 seconds | Technical Architect |
| Playwright memory consumption | HIGH | HIGH | - Semaphore limit: max 10 concurrent PDFs - Worker instance memory monitoring - Graceful degradation if memory high - Browser instance pooling | Technical Architect |
| Swedish character encoding (åäö) | MEDIUM | MEDIUM | - UTF-8 throughout entire pipeline - Font embedding in PDF - Visual testing with Swedish content - Sample invoices with all Swedish special chars | QA Team |
| Template injection security | LOW | CRITICAL | - Handlebars safe mode (no eval) - Template sanitization on upload - No dynamic helper registration - Security code review | Security Officer |
| Missing template category | LOW | MEDIUM | - Fall back to default "invoice" template - Log warning for missing category - Template category validation | Product Owner |
3.5 FR-005: Email Delivery Service
Requirement: The EmailDeliveryService shall listen to email-queue, download PDF from blob storage, send via SendGrid with Swedish-localized email template, track delivery status, and retry on transient failures with fallback to postal queue.
Priority: HIGH
Service Specifications:
Trigger: Message in email-queue
Input: Invoice ID, recipient email, organization ID
Output: Email sent via SendGrid, delivery status updated
Processing Steps:
1. Dequeue message from email-queue
2. Download PDF: {org}-invoices-{year}/{month}/{day}/{invoice-id}.pdf
3. Load organization email configuration
4. Load email template (Swedish)
5. Create SendGrid message:
- From: noreply@{org-domain}.com
- To: {customer-email}
- Subject: "Faktura {invoiceNumber} från {orgName}"
- Body: HTML template with invoice summary
- Attachment: invoice-{invoiceNumber}.pdf
6. Send via SendGrid API
7. Handle response:
- Success (2xx): Update invoice status="delivered", log messageId
- Rate limit (429): Re-queue with Retry-After delay
- Transient error (5xx): Retry with exponential backoff (3x)
- Permanent error (4xx): Move to postal-bulk-queue (fallback)
8. Update invoice metadata with delivery attempt
9. Delete from email-queue (on success or permanent failure)
Email Template (Swedish):
<!DOCTYPE html> <html lang="sv"> <head> <meta charset="UTF-8"> <title>Faktura</title> </head> <body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;"> <div style="background: #0066CC; color: white; padding: 20px; text-align: center;"> <h1>Faktura från {{organizationName}}</h1> </div> <div style="padding: 20px;"> <p>Hej {{customerName}},</p> <p>Din faktura för perioden {{periodStart}} till {{periodEnd}} är nu tillgänglig.</p> <table style="width: 100%; margin: 20px 0; border-collapse: collapse;"> <tr style="background: #f5f5f5;"> <td style="padding: 10px; border: 1px solid #ddd;"><strong>Fakturanummer:</strong></td> <td style="padding: 10px; border: 1px solid #ddd;">{{invoiceNumber}}</td> </tr> <tr> <td style="padding: 10px; border: 1px solid #ddd;"><strong>Förfallodatum:</strong></td> <td style="padding: 10px; border: 1px solid #ddd;">{{dueDate}}</td> </tr> <tr style="background: #f5f5f5;"> <td style="padding: 10px; border: 1px solid #ddd;"><strong>Att betala:</strong></td> <td style="padding: 10px; border: 1px solid #ddd;"><strong>{{totalAmount}} SEK</strong></td> </tr> </table> <p><strong>Betalningsinformation:</strong></p> <p>Bankgiro: {{bankAccount}}<br> OCR-nummer: {{ocrNumber}}</p> <p>Din faktura finns bifogad som PDF.</p> <p>Vid frågor, kontakta oss på {{supportEmail}} eller {{supportPhone}}.</p> <p>Med vänlig hälsning,<br> {{organizationName}}</p> </div> <div style="background: #f5f5f5; padding: 15px; text-align: center; font-size: 12px; color: #666;"> <p>Detta är ett automatiskt meddelande. Svara inte på detta e-postmeddelande.</p> </div> </body> </html>
Acceptance Criteria:
| Criterion | Validation Method | Test Data | Expected Result |
|---|---|---|---|
| Listens to email-queue | Send message, verify processing | Queue message | Message dequeued |
| Downloads PDF from correct blob path | Verify blob access logged | Invoice with PDF | PDF downloaded |
| Sends via SendGrid API | Mock SendGrid, verify API call | Email invoice | SendGrid API called |
| PDF attached to email | Receive test email, check attachment | Email invoice | PDF attached |
| Subject includes invoice number (Swedish) | Check email subject | Invoice 123 | "Faktura 123 från..." |
| From address uses org domain | Check email headers | Org config | From: noreply@acme.com |
| Reply-to set to org support | Check email headers | Org config | Reply-To: support@acme.com |
| Swedish email template used | Check email body | Email invoice | Swedish text |
| Retry 2x on transient failure (1min, 5min) | Force 500 error from SendGrid | Failing email | 2 retries logged |
| Fallback to postal on permanent failure | Force 400 error (invalid email) | Bad email | Postal queue message |
| Delivery status tracked in invoice metadata | Check metadata after send | Delivered invoice | deliveryAttempts array updated |
| SendGrid messageId logged | Check invoice metadata | Delivered invoice | providerMessageId present |
| Rate limit handling (429) | Simulate rate limit | Many emails | Re-queued with delay |
| Email size validation (<25MB) | Large PDF attachment | 30MB PDF | Error or compression |
SendGrid Configuration:
{
"sendgrid": {
"apiKey": "{{from-azure-keyvault}}",
"fromEmail": "noreply@{org-domain}.com",
"fromName": "{organizationName}",
"replyTo": "support@{org-domain}.com",
"tracking": {
"clickTracking": false,
"openTracking": true,
"subscriptionTracking": false
},
"mailSettings": {
"sandboxMode": false,
"spamCheck": {
"enable": true,
"threshold": 5
}
}
}
}
Dependencies:
- SendGrid account with Nordic IP reputation
- Azure Storage Queue:
email-queue - Email templates in Swedish (+ future: Norwegian, Danish, Finnish)
- Organization email configuration in blob
- Fallback queue routing to postal
Risks & Mitigation (Nordic Email Deliverability):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Swedish ISP spam filtering (Telia, Tele2, Telenor) | MEDIUM | HIGH | - Dedicated IP warmup (2-week ramp) - SPF: include:sendgrid.net - DKIM signing enabled - DMARC p=quarantine policy - Monitor bounce rates by ISP - Request whitelisting from major ISPs | Operations Manager |
| SendGrid rate limits (enterprise plan needed) | MEDIUM | MEDIUM | - Enterprise plan: 2M emails/month - Queue-based pacing - Monitor daily send volume - Distribute sends over 24 hours - Priority queue for SLA customers | Product Owner |
| PDF attachment size (>25MB) | LOW | LOW | - Compress PDFs with Ghostscript - Target: <5MB per invoice - Alert if PDF >20MB - Fallback: send download link | Technical Architect |
| Email template rendering errors | LOW | MEDIUM | - Template validation on deployment - Fallback to plain text if HTML fails - Error monitoring - Sample sends for all templates | QA Team |
| Customer email address invalid | MEDIUM | LOW | - Email validation before send - Skip email, go directly to postal - Log invalid addresses for org to correct | Product Owner |
3.6 FR-006: Postal Delivery Service (21G Bulk Integration)
Requirement: The PostalDeliveryService shall listen to postal-bulk-queue, collect invoices for bulk processing, create ZIP archive with PDFs and XML metadata in 21G format, upload to 21G SFTP server at scheduled times (12:00 and 20:00 Swedish time), and track delivery confirmations.
Priority: HIGH
Service Specifications:
Trigger: Scheduled execution (12:00 and 20:00 CET/CEST)
Input: All messages in postal-bulk-queue accumulated since last run
Output: ZIP file uploaded to 21G SFTP, delivery confirmations tracked
Processing Steps:
1. Scheduled trigger (12:00 and 20:00 Swedish time)
2. Fetch all messages from postal-bulk-queue (batch retrieval)
3. For each invoice in queue:
a. Download PDF: {org}-invoices-{year}/{month}/{day}/{invoice-id}.pdf
b. Download invoice JSON for metadata
c. Validate recipient address is complete
d. Add to 21G batch collection
4. Group by organization (21G requires org-specific batches)
5. For each organization batch:
a. Create 21G XML metadata file with all invoice details
b. Create ZIP archive: {org-code}_{date}_{sequence}.zip
- Contains: invoice1.pdf, invoice2.pdf, ..., metadata.xml
c. Upload ZIP to 21G SFTP: /incoming/{org-code}/
d. Verify upload success
e. Update all invoice statuses: status="postal_sent"
f. Delete messages from postal-bulk-queue
6. Log bulk send statistics to Application Insights
7. Send notification email to organization (bulk send report)
21G ZIP Structure:
ACME_20251121_001.zip
├── metadata.xml (21G format)
├── invoice_001.pdf
├── invoice_002.pdf
├── invoice_003.pdf
└── ...
21G Metadata XML Format:
<?xml version="1.0" encoding="UTF-8"?> <PrintBatch xmlns="urn:21g:print:batch:1.0"> <BatchHeader> <BatchId>ACME_20251121_001</BatchId> <OrganizationCode>ACME</OrganizationCode> <CreationDate>2025-11-21T12:00:00</CreationDate> <TotalDocuments>150</TotalDocuments> <ServiceLevel>Economy</ServiceLevel> </BatchHeader> <Documents> <Document> <DocumentId>invoice_001.pdf</DocumentId> <DocumentType>Invoice</DocumentType> <Recipient> <Name>Medeni Schröder</Name> <Street>Strandbo 63B</Street> <PostalCode>352 58</PostalCode> <City>Växjö</City> <Country>SE</Country> </Recipient> <PrintOptions> <Format>A4</Format> <Color>false</Color> <Duplex>false</Duplex> </PrintOptions> </Document> <!-- ... more documents --> </Documents> </PrintBatch>
Acceptance Criteria:
| Criterion | Validation Method | Test Data | Expected Result |
|---|---|---|---|
| Scheduled execution at 12:00 Swedish time | Check execution logs | Scheduled time | Runs at 12:00 CET/CEST |
| Scheduled execution at 20:00 Swedish time | Check execution logs | Scheduled time | Runs at 20:00 CET/CEST |
| Fetches all messages from postal-bulk-queue | Queue 100 messages, verify all fetched | 100 postal invoices | All 100 fetched |
| Downloads PDFs from blob storage | Verify blob access | Postal invoices | All PDFs downloaded |
| Validates recipient address complete | Invoice with missing city | Incomplete address | Skipped with error log |
| Groups by organization | Mix of Org A and Org B invoices | Multi-org batch | Separate ZIPs per org |
| Creates 21G XML metadata | Verify XML structure | Postal batch | Valid 21G XML |
| Creates ZIP archive | Verify ZIP contents | Postal batch | PDFs + metadata.xml |
| Uploads to 21G SFTP | Mock SFTP, verify upload | ZIP file | File uploaded |
| Verifies upload success | Check SFTP confirmation | Uploaded ZIP | Confirmation received |
| Updates invoice status to "postal_sent" | Check invoice metadata | Sent invoices | Status updated |
| Deletes messages from queue | Check queue after processing | Processed batch | Queue empty |
| Logs bulk statistics | Check Application Insights | Processed batch | Statistics logged |
| Sends org notification email | Check email received | Processed batch | Email with counts |
| Handles SFTP connection errors | Simulate SFTP down | Postal batch | Retry logged, alert sent |
| Respects 21G batch size limits | Create large batch | 10,000 invoices | Split into multiple ZIPs |
21G Integration Specifications:
SFTP Connection:
- Host: sftp.21g.se (or provider-specific)
- Port: 22
- Authentication: SSH key (stored in Azure Key Vault)
- Directory structure: /incoming/{org-code}/
- File naming: {org-code}{YYYYMMDD}{sequence}.zip
21G SLA:
- Processing time: 24-48 hours
- Confirmation: Email notification when processed
- Tracking: Available via 21G portal
Dependencies:
- 21G SFTP account and credentials
- Azure Storage Queue:
postal-bulk-queue - Scheduled worker (Azure Container Apps with CRON)
- ZIP file creation library
- 21G XML schema compliance
- Email notification service
Risks & Mitigation (Nordic Postal Context):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| 21G SFTP connectivity issues | LOW | HIGH | - Retry logic (3 attempts with 5min delay) - Secondary SFTP credentials - Alert on connection failure - Manual upload procedure documented - 21G support contact documented | Operations Manager |
| Swedish postal delays (holidays, strikes) | MEDIUM | MEDIUM | - Set customer expectations (5-7 days) - Monitor 21G processing SLA - Track delivery confirmations - Escalation for >10 days - Alternative print partner identified | Product Owner |
| Incomplete recipient addresses | MEDIUM | LOW | - Address validation before queueing - Skip invalid addresses - Alert organization of invalid addresses - Provide address correction interface | Product Owner |
| 21G format specification changes | LOW | MEDIUM | - Version 21G XML schema - Monitor 21G API announcements - Test uploads to 21G staging - 21G account manager liaison | Technical Architect |
| ZIP file corruption | LOW | HIGH | - SHA-256 checksum in metadata - Verify ZIP integrity before upload - Keep ZIP in blob for 30 days - 21G confirms successful unzip | Technical Architect |
3.7 FR-007: Distribution Routing Logic
Requirement: The system shall determine the appropriate distribution queue for each invoice based on organization configuration, customer preferences, and distribution type (invoice vs document) following Swedish regulatory requirements.
Priority: HIGH
Routing Decision Tree:
Invoice Distribution Routing
│
├─ Check customer preference (if available)
│ ├─ Preference = "digital" → email-queue (or kivra-queue in future)
│ └─ Preference = "postal" → postal-bulk-queue
│
├─ Check organization default channels (from config)
│ ├─ Priority 1: email
│ │ └─ Has valid email? → email-queue
│ ├─ Priority 2: kivra (Phase 2)
│ │ └─ Kivra user? → kivra-queue
│ └─ Priority 3: postal
│ └─ postal-bulk-queue
│
├─ Document type consideration
│ ├─ Invoice (faktura) → All channels available
│ └─ Confirmation letter → Email/postal only
│
└─ Swedish regulatory compliance
└─ Customer always has right to postal ("rätt till pappersfaktura")
Queue Selection Logic:
public async Task<string> DetermineDistributionQueueAsync( InvoiceDistribution distribution, OrganizationConfig config) { // Customer preference overrides (if explicitly set) if (distribution.CustomerPreference == "postal") return "postal-bulk-queue"; // Try channels in priority order var priorities = config.DeliveryChannels.ChannelPriority .OrderBy(p => p.Priority); foreach (var channel in priorities) { switch (channel.Channel) { case "email": if (!string.IsNullOrEmpty(distribution.CustomerEmail) && IsValidEmail(distribution.CustomerEmail)) { return "email-queue"; } break; case "kivra": // Phase 2 if (await IsKivraUserAsync(distribution.CustomerPersonnummer)) { return "kivra-queue"; } break; case "efaktura": // Phase 2 (B2B only) if (distribution.CustomerType == "business" && !string.IsNullOrEmpty(distribution.OrganizationNumber)) { return "efaktura-queue"; } break; case "postal": if (distribution.IsCompleteAddress()) { return "postal-bulk-queue"; } break; } } // Ultimate fallback: postal (Swedish law requires paper option) return "postal-bulk-queue"; }
Acceptance Criteria:
| Criterion | Validation Method | Test Data | Expected Result |
|---|---|---|---|
| Customer preference honored | Set preference="postal" | Email-enabled invoice | Routes to postal queue |
| Organization priority followed | Priority: [email, postal] | Valid email | Routes to email queue |
| Email validated before routing | Invalid email address | bad-email@invalid | Routes to postal queue |
| Complete address required for postal | Missing postal code | Incomplete address | Error logged, skipped |
| Document type considered | Confirmation letter | Non-invoice doc | Only email/postal |
| Swedish postal fallback | All digital channels fail | Failed digital | postal-bulk-queue |
| Business invoices support e-faktura (future) | Organization number present | B2B invoice | efaktura-queue (Phase 2) |
| Routing decision logged | Check logs | Any invoice | Decision reason logged |
Dependencies:
- Organization delivery configuration
- Customer preference storage (future)
- Email validation library
- Address validation library (Swedish postal codes)
- Kivra user lookup API (Phase 2)
Risks & Mitigation:
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Invalid email addresses (>5% in Nordic utilities) | HIGH | LOW | - Email validation regex - Automatic postal fallback - Report invalid emails to organization - Customer data quality improvement program | Product Owner |
| Incomplete postal addresses | MEDIUM | MEDIUM | - Address validation against Swedish postal database - Skip invalid addresses with alert - Organization notification of incomplete addresses | Product Owner |
| Swedish "rätt till pappersfaktura" compliance | LOW | CRITICAL | - Always enable postal as fallback - Never force digital-only - Document compliance in privacy policy - Legal review of routing logic | Legal/Compliance |
3.8 FR-008: Blob Concurrency Control (Note: Read-Only, No Concurrent Updates)
Requirement: The system shall use blob leases to ensure exclusive access during batch processing, preventing concurrent worker instances from processing the same 32-item batch.
Priority: HIGH
Clarification: Based on your feedback, there are no concurrent updates to files - workers only read invoice JSON files. The blob lease is used to ensure only one worker processes a given 32-item batch from the queue.
Lease Implementation:
public async Task ProcessBatchItemsAsync(BatchItemsMessage message) { var lockBlobPath = $"{message.OrganizationId}-batches-{year}/{month}/{day}/{message.BatchId}/locks/{message.MessageId}.lock"; BlobLease lease = null; try { // Acquire lease (5-minute duration) lease = await _blobLockService.AcquireLockAsync( containerName: $"{message.OrganizationId}-batches-{year}", blobName: lockBlobPath, leaseDuration: TimeSpan.FromMinutes(5)); // Process 32 invoices (read-only operations) foreach (var invoiceId in message.InvoiceIds) { // Read invoice JSON from blob (no updates to JSON) var invoiceJson = await _blobStorage.DownloadJsonAsync(invoiceId); // Render and generate (creates new HTML/PDF blobs) await RenderAndGenerateAsync(invoiceJson); // No concurrent update risk - creating new blobs only } // Update batch metadata (ETag-based optimistic concurrency) await UpdateBatchMetadataAsync(message.BatchId, meta => { meta.Statistics.ProcessedItems += message.InvoiceIds.Count; }); } finally { if (lease != null) { await _blobLockService.ReleaseLockAsync(lease); } } }
Acceptance Criteria:
| Criterion | Validation Method | Test Data | Expected Result |
|---|---|---|---|
| Acquires blob lease before processing | Start processing, check lease | 32-item batch | Lease acquired |
| Lease duration is 5 minutes | Check lease properties | Any batch | Duration = 5 min |
| Only one worker processes batch | Send same message to 2 workers | Duplicate message | One succeeds, one waits |
| Lease renewed for long processing | Process 32 items slowly | Slow batch | Lease renewed |
| Lease released on completion | Check lease after processing | Completed batch | Lease released |
| Lease released on error | Force error during processing | Failing batch | Lease released |
| Different batches process in parallel | Queue 10 batches | 10 x 32 items | All process concurrently |
| Batch metadata updates use ETags | Concurrent metadata updates | 2 workers update stats | No lost updates |
Dependencies:
- Azure Blob Storage lease API
- ETag-based optimistic concurrency for metadata updates
- Retry logic for lease acquisition conflicts
3.9 FR-009: Queue Message Handling
Requirement: The system shall process queue messages with proper visibility timeouts, automatic retry on failure, poison queue handling, and message deduplication.
Priority: HIGH
Queue Configuration:
| Queue Name | Purpose | Visibility Timeout | Max Delivery Count | Dead Letter Queue |
|---|---|---|---|---|
batch-upload-queue | Triggers ParserService | 10 minutes | 3 | poison-queue |
batch-items-queue | Triggers DocumentGenerator (32 items) | 5 minutes | 3 | poison-queue |
email-queue | Triggers EmailService | 2 minutes | 3 | poison-queue |
postal-bulk-queue | Collected for 21G bulk send | N/A (batch retrieval) | 1 | poison-queue |
poison-queue | Failed messages for manual review | N/A | 0 | None |
Message Format Standard:
{
"messageId": "uuid",
"messageType": "batch.upload|batch.items|email.delivery|postal.delivery",
"version": "1.0",
"timestamp": "2025-11-21T10:30:00Z",
"data": {
// Message-specific payload
},
"metadata": {
"correlationId": "uuid",
"organizationId": "uuid",
"retryCount": 0,
"enqueuedAt": "2025-11-21T10:30:00Z"
}
}
Retry Policy (Exponential Backoff):
| Attempt | Delay | Total Elapsed |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 60 seconds | 1 minute |
| 3 | 300 seconds | 6 minutes |
| 4 | 900 seconds | 21 minutes |
| Failed | Poison queue | - |
Acceptance Criteria:
| Criterion | Validation Method | Test Data | Expected Result |
|---|---|---|---|
| Messages have proper visibility timeout | Check queue properties | Any message | Correct timeout set |
| Failed messages retry automatically | Force error, verify retry | Failing message | 3 retries attempted |
| Retry count incremented | Check message metadata | Retried message | retryCount incremented |
| Exponential backoff applied | Measure retry delays | Failing message | 1min, 5min, 15min |
| After 3 retries, moved to poison queue | Force permanent failure | Failing message | In poison queue |
| Poison queue triggers alert | Message in poison queue | Failed message | Alert email sent |
| Support team notified | Check alert recipients | Poison message | Support receives email |
| No duplicate processing (idempotent) | Send duplicate message | Same invoice ID | Processed once |
| Correlation ID traces through system | Follow message across queues | Any message | Same correlationId |
Poison Queue Handling:
{
"messageId": "uuid",
"originalMessageType": "batch.items",
"failedAt": "2025-11-21T10:50:00Z",
"retryCount": 3,
"lastError": {
"code": "TEMPLATE_RENDERING_FAILED",
"message": "Required variable 'customer.address.street' not found in template",
"stackTrace": "...",
"attemptTimestamps": [
"2025-11-21T10:35:00Z",
"2025-11-21T10:36:00Z",
"2025-11-21T10:41:00Z",
"2025-11-21T10:56:00Z"
]
},
"originalMessage": {
// Full original message for debugging
},
"metadata": {
"correlationId": "uuid",
"alertSent": true,
"alertRecipients": ["support@egflow.com"],
"manualReviewRequired": true
}
}
Dependencies:
- Azure Storage Queues with dead-letter queue support
- Alert service for poison queue notifications
- Monitoring dashboard for poison queue depth
3.10 FR-010: Health Check Endpoints
Requirement: All services shall expose health check endpoints that verify connectivity to dependencies (database, blob storage, queues) and return health status for Azure Traffic Manager and monitoring.
Priority: HIGH
API Endpoint: GET /health
Response Format (Healthy):
{
"status": "Healthy",
"timestamp": "2025-11-21T10:30:00Z",
"version": "1.0.0",
"checks": {
"blobStorage": {
"status": "Healthy",
"responseTime": "45ms",
"lastChecked": "2025-11-21T10:30:00Z"
},
"storageQueue": {
"status": "Healthy",
"responseTime": "32ms",
"queueDepth": 150
},
"postgresql": {
"status": "Healthy",
"responseTime": "12ms",
"activeConnections": 8
},
"keyVault": {
"status": "Healthy",
"responseTime": "67ms"
}
},
"environment": "production",
"region": "westeurope"
}
Response Format (Unhealthy):
{
"status": "Unhealthy",
"timestamp": "2025-11-21T10:30:00Z",
"checks": {
"blobStorage": {
"status": "Unhealthy",
"error": "Connection timeout after 5000ms",
"lastChecked": "2025-11-21T10:30:00Z"
},
"postgresql": {
"status": "Healthy",
"responseTime": "15ms"
}
}
}
Acceptance Criteria:
| Criterion | Validation Method | Test Data | Expected Result |
|---|---|---|---|
| Returns 200 when all checks healthy | All dependencies up | N/A | 200 OK, status="Healthy" |
| Returns 503 when any check unhealthy | Stop database | N/A | 503 Service Unavailable |
| Checks blob storage connectivity | Disconnect blob storage | N/A | blobStorage.status="Unhealthy" |
| Checks queue connectivity | Disable queue access | N/A | storageQueue.status="Unhealthy" |
| Checks PostgreSQL connectivity | Stop database | N/A | postgresql.status="Unhealthy" |
| Checks Key Vault access | Revoke Key Vault permissions | N/A | keyVault.status="Unhealthy" |
| Response time < 1 second | Performance test | N/A | Health check completes quickly |
| Traffic Manager uses for routing | Simulate region failure | N/A | Traffic routes to healthy region |
| Includes environment and region | Check response body | N/A | Environment and region present |
Dependencies:
- Health check library (ASP.NET Core HealthChecks)
- Azure Traffic Manager configuration
- Monitoring integration
3.11 FR-011: Template Category Management
Requirement: The system shall support template categories (e.g., "Invoice", "Confirmation Letter", "Reminder") to group related templates and enable dynamic template selection based on document type.
Priority: MEDIUM
API Endpoint: GET /organizations/{organizationId}/template-categories
Response Format:
{
"success": true,
"data": {
"categories": [
{
"categoryId": "uuid",
"categoryName": "invoice",
"displayName": "Faktura",
"description": "Standard invoice template",
"activeTemplateId": "uuid",
"activeTemplateVersion": "2.1.0",
"templateCount": 3
},
{
"categoryId": "uuid",
"categoryName": "confirmation",
"displayName": "Bekräftelsebrev",
"description": "Contract confirmation letter",
"activeTemplateId": "uuid",
"activeTemplateVersion": "1.0.0",
"templateCount": 1
},
{
"categoryId": "uuid",
"categoryName": "reminder",
"displayName": "Påminnelse",
"description": "Payment reminder",
"activeTemplateId": null,
"templateCount": 0
}
]
}
}
Template Category Determination Logic:
Document Type → Template Category Mapping:
Invoice (faktura) → "invoice" template
Confirmation letter (bekräftelsebrev) → "confirmation" template
Payment reminder (påminnelse) → "reminder" template
Termination notice (uppsägning) → "termination" template
Contract change (avtalsändring) → "contract_change" template
Default: If category not found → use "invoice" template
Acceptance Criteria:
| Criterion | Validation Method | Test Data | Expected Result |
|---|---|---|---|
| Lists all categories for organization | GET /template-categories | Org with 3 categories | 3 categories returned |
| Shows active template per category | Check activeTemplateId | Category with active template | Template ID present |
| Returns null for unused categories | Check reminder category | No reminder template | activeTemplateId: null |
| Includes template count | Verify count | Category with 3 versions | templateCount: 3 |
| Category names localized (Swedish) | Check displayName | All categories | Swedish names |
3.12 FR-012: Git Branching Strategy (Development Workflow)
Requirement: The development team shall follow a structured Git branching strategy with development, staging, and main branches, following industry best practices for continuous integration and deployment.
Priority: HIGH
Branch Structure (Updated per Nov 20 decision):
main (production)
↑
└── staging (acceptance testing)
↑
└── development (integration testing)
↑
└── feature/* (short-lived branches)
Branch Policies:
| Branch | Purpose | Merge From | Deploy To | Protection |
|---|---|---|---|---|
| feature/ | Individual features | N/A | N/A | None (local only) |
| development | Integration testing | feature/* | Dev environment | PR required, 1 approval |
| staging | Acceptance testing (internal) | development | Staging environment | PR required, 2 approvals, all tests pass |
| main | Production | staging | Production (West + North Europe) | PR required, 3 approvals, security scan, all tests pass |
Deployment Flow:
Developer → feature/GAS-12345-batch-upload
↓
PR to development
↓
CI/CD: Build, Test, Deploy to Dev
↓
Integration testing in Dev
↓
PR to staging (approved by Product Owner)
↓
CI/CD: Build, Test, Deploy to Staging
↓
UAT testing in Staging (European team only)
↓
PR to main (approved by Product Owner + Architect + Ops)
↓
CI/CD: Build, Security Scan, Deploy to Prod (West Europe)
↓
Deploy to Prod (North Europe) - manual approval
Acceptance Criteria:
| Criterion | Validation Method | Expected Result |
|---|---|---|
| Feature branches merge to development only | Attempt direct merge to staging | PR blocked |
| Development branch auto-deploys to dev env | Merge to development | Dev environment updated |
| Staging branch requires 2 approvals | Create PR to staging | Cannot merge with 1 approval |
| Main branch requires 3 approvals | Create PR to main | Cannot merge with 2 approvals |
| All tests must pass before staging merge | Failing test in PR | Merge blocked |
| Security scan required for main | PR to main | SAST scan runs |
| Feature branches deleted after merge | Merge feature branch | Branch auto-deleted |
Dependencies:
- Azure DevOps or GitHub repository
- CI/CD pipeline configuration
- Branch protection policies
- Code review requirements
3.13 FR-013: Synthetic Test Data Generation (GDPR Compliance)
Requirement: The system shall provide a synthetic data generation tool that creates realistic but fake Swedish invoice data (personnummer, addresses, names) for use in staging and development environments, with zero production data copying.
Priority: CRITICAL
Synthetic Data Requirements:
Swedish Personnummer Generation:
Format: YYMMDD-XXXX
- YY: Year (00-99)
- MM: Month (01-12)
- DD: Day (01-31)
- XXXX: Last 4 digits with Luhn checksum
Generation Rules:
- Must pass Luhn algorithm validation
- Must NOT match any real personnummer
- Use test ranges: 19000101-19991231 (obviously fake dates)
- Flag as test: personnummer starts with "19"
Swedish Address Generation:
Street names: Random from Swedish street database
- Storgatan, Kungsgatan, Vasagatan, Drottninggatan...
- House numbers: 1-150
- Apartment: A-Z (optional)
Cities: Top 50 Swedish cities
- Stockholm, Göteborg, Malmö, Uppsala, Västerås...
Postal codes: Valid format (XXX XX) but non-existent ranges
- Use: 00X XX range (invalid but correct format)
Examples:
- Storgatan 45, 001 23 Stockholm
- Vasagatan 12 A, 002 45 Göteborg
Synthetic Invoice Data:
Invoice numbers: Test prefix "TEST-" + sequential
Amounts: Random between 100-5000 SEK
Consumption: Random 100-2000 kWh (residential realistic)
Metering points: Test range 735999999999999XXX
Email addresses: {firstname}.{lastname}@example-test.se
Phone numbers: +46701234XXX (test range)
Acceptance Criteria:
| Criterion | Validation Method | Expected Result |
|---|---|---|
| Generates valid personnummer (Luhn check) | Validate 1000 generated numbers | All pass Luhn validation |
| Personnummer obviously fake | Review generated numbers | All start with "19" (invalid birth years) |
| Addresses realistic but invalid | Check against real postal database | No matches found |
| Email addresses use test domain | Check generated emails | All @example-test.se |
| Phone numbers in test range | Check generated phones | All +467012340XX |
| Can generate 10K invoice batch | Generate full batch | 10K valid invoices |
| Zero real data in output | Scan for real personnummer patterns | No real data found |
| Reproducible (seed-based) | Generate twice with same seed | Identical output |
Dependencies:
- Swedish personnummer validation library
- Swedish postal code database (for validation, not generation)
- Random data generation library
Risks & Mitigation:
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Accidental real data generation | LOW | CRITICAL | - Validation against known real ranges - Visual "TEST DATA" watermark on PDFs - Automated scanning for real personnummer - Code review of generation logic | Security Officer |
| Unrealistic test scenarios | MEDIUM | MEDIUM | - Generate edge cases library - Long names, special characters - Missing optional fields - Various consumption patterns | QA Team |
| Offshore team needs production debugging | MEDIUM | MEDIUM | - European team creates synthetic scenarios - Screen sharing for production issues - Never copy production data - Comprehensive logs without PII | Operations Manager |
4. Non-Functional Requirements
4.1 NFR-001: Performance Targets
Requirement: The system shall meet specified performance targets for processing throughput, API latency, and rendering speed to support high-volume Nordic utilities operations.
Priority: HIGH
Performance Targets:
| Metric | Target | Measurement Method | Acceptance Threshold | Test Scenario |
|---|---|---|---|---|
| Batch Processing Throughput | 10M invoices/month | Monthly invoice count in Application Insights | ≥ 10M in peak month | Production monitoring |
| 100K Batch Processing Time | < 2 hours | Timestamp diff (queuedAt to completedAt) | ≤ 120 minutes | Load test TC-200 |
| API Response Time (p50) | < 200ms | Application Insights percentiles | ≤ 200ms | Load test TC-201 |
| API Response Time (p95) | < 500ms | Application Insights percentiles | ≤ 500ms | Load test TC-202 |
| API Response Time (p99) | < 1000ms | Application Insights percentiles | ≤ 1000ms | Load test TC-203 |
| PDF Generation Time (p95) | < 5 seconds/invoice | Custom metric tracking | ≤ 5 seconds | Render test TC-204 |
| Handlebars Rendering (p95) | < 2 seconds/invoice | Custom metric tracking | ≤ 2 seconds | Render test TC-205 |
| Queue Processing Lag | < 5 minutes | Queue depth / throughput calculation | ≤ 5 minutes | Queue monitoring |
| Database Query Time (p95) | < 100ms | PostgreSQL slow query log | ≤ 100ms | Query analysis |
| ParserService: 10K batch | < 2 minutes | Parse duration measurement | ≤ 120 seconds | Parser test TC-206 |
Load Testing Scenarios:
Scenario 1: Steady State (Normal Month)
- Duration: 24 hours
- Load: 333K invoices distributed evenly
- Concurrent batches: 5-10
- Expected: All targets met, no errors
Scenario 2: Peak Load (Heating Season)
- Duration: 8 hours
- Load: 314K invoices (concentrated first week)
- Concurrent batches: 20+
- Expected: 2-hour SLA met, >99% success rate
Scenario 3: Spike Test
- Duration: 30 minutes
- Load: Sudden 10 batch uploads (50K invoices)
- Expected: System auto-scales, processes without degradation
Acceptance Criteria:
| Criterion | Validation Method | Target |
|---|---|---|
| Load test with 100K batch completes | End-to-end test | ≤ 2 hours |
| API maintains p95 < 500ms under load | Concurrent API requests (1000 RPS) | ≤ 500ms |
| System processes 10M in peak month | Production monitoring (Oct-Mar) | ≥ 10M |
| No performance degradation with 50 orgs | 50 orgs upload simultaneously | All SLAs met |
| Worker auto-scaling maintains lag < 5 min | Monitor queue depth during peaks | Lag ≤ 5 min |
| PDF generation stays within target | Render 1000 PDFs, measure p95 | ≤ 5 seconds |
Dependencies:
- Azure Container Apps auto-scaling configuration
- Application Insights performance monitoring
- Load testing tool (NBomber, k6, or JMeter)
- Blob storage premium tier for high IOPS
Risks & Mitigation (Nordic Peak Season):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Heating season peaks exceed capacity (Oct-Mar) | MEDIUM | HIGH | - Historical data analysis for peak prediction - Pre-warm workers 1st/last week of month - Priority queue for SLA customers - Customer communication: off-peak scheduling - Capacity planning review quarterly | Operations Manager |
| Template complexity slows rendering | MEDIUM | HIGH | - Template performance guidelines - POC testing with customer templates - Recommend simple templates - Compiled template caching - Parallel rendering for 32 items | Technical Architect |
| Playwright memory issues at scale | HIGH | HIGH | - Semaphore: max 10 concurrent PDFs - Worker memory limit: 2GB - Browser instance pooling - Monitor memory usage - Scale horizontally (more workers) | Technical Architect |
| PostgreSQL connection exhaustion | MEDIUM | MEDIUM | - Connection pooling (max 50 per service) - Monitor active connections - Timeout settings (30 seconds) - Consider read replicas for heavy queries | Technical Architect |
4.2 NFR-002: Scalability & Auto-Scaling
Requirement: The system shall scale horizontally without manual intervention to handle peak loads during Nordic heating season (October-March) and monthly invoice cycles.
Priority: HIGH
Scaling Configuration:
| Component | Min Instances | Max Instances | Trigger Metric | Threshold | Scale Up Time | Scale Down Time |
|---|---|---|---|---|---|---|
| CoreApiService | 5 | 20 | CPU Utilization OR Request Rate | 70% OR 1000 RPS | 2 minutes | 10 minutes |
| ParserService | 2 | 10 | Queue Length (batch-upload-queue) | Length > 0 | 1 minute | 5 minutes |
| DocumentGenerator | 2 | 100 | Queue Length (batch-items-queue) | Length > 32 | 1 minute | 5 minutes |
| EmailService | 5 | 50 | Queue Length (email-queue) | Length > 50 | 1 minute | 5 minutes |
| PostalService | 1 | 3 | Scheduled (not queue-based) | 12:00, 20:00 CET | N/A | After completion |
Peak Load Capacity:
Normal Load (non-heating season, mid-month):
- 333K invoices/day average
- 5-10 concurrent batches
- Worker instances: 10-20 total
Peak Load (heating season, first/last week):
- 2.2M invoices/week (95% of monthly volume)
- 314K invoices/day
- 20+ concurrent batches
- Worker instances: 80-100 total
Scaling Calculation:
Peak Day: 314,000 invoices
Processing time per invoice: 10 seconds (parse + render + PDF + deliver)
Total processing time: 314,000 × 10s = 872 hours
Target completion: 8 hours
Required workers: 872 / 8 = 109 workers
With 32 items/worker: 109 × 32 = 3,488 items processing simultaneously
Acceptance Criteria:
| Criterion | Validation Method | Target |
|---|---|---|
| Auto-scaling triggered on queue depth | Monitor scaling events | Scales within 2 min |
| Scaling up completes within 2 minutes | Measure from trigger to ready | ≤ 2 minutes |
| Scaling down after 10 min low load | Monitor scale-down timing | ≥ 10 minutes |
| Performance maintained during scaling | Monitor API latency during scale events | No degradation |
| No message loss during scaling | Count messages before/after | 100% preserved |
| Pre-warming for known peaks | Schedule scale-up 1st/last week | Workers ready |
| Max 100 DocumentGenerator instances | Verify max instance count | ≤ 100 |
Pre-Warming Strategy (Heating Season):
Monthly Schedule:
- Day 1-7: Pre-warm to 50 instances at 00:00
- Day 8-23: Scale based on queue (2-20 instances)
- Day 24-31: Pre-warm to 50 instances at 00:00
Heating Season (Oct-Mar): Double pre-warm levels
- Day 1-7: Pre-warm to 80 instances
- Day 24-31: Pre-warm to 80 instances
Dependencies:
- Azure Container Apps with KEDA (Kubernetes Event-Driven Autoscaling)
- Queue depth monitoring
- Historical load data for pre-warming schedule
Risks & Mitigation:
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Scale-up too slow for sudden spike | MEDIUM | HIGH | - Pre-warm during known peaks - Keep min instances higher during peak season - Queue backpressure (503) if overloaded - Customer scheduling guidance | Operations Manager |
| Azure Container Apps 100-instance limit | LOW | HIGH | - Priority queue for SLA customers - Queue backpressure to throttle intake - Consider split by organization - Plan for Phase 2: dedicated worker pools | Technical Architect |
| Cost escalation during sustained peaks | MEDIUM | MEDIUM | - Cost alerts at thresholds - Auto-scale down aggressively - Reserved instances for base load - Monitor cost per invoice | Finance Controller |
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, messages retained | Ops Manager |
| SendGrid Complete Outage | < 2 hours | 0 (fallback to postal) | Route all email invoices to postal queue | Ops Team |
| 21G SFTP Unavailable | < 4 hours | 0 (retry scheduled) | Retry at next scheduled time (12:00/20:00) | Ops Team |
Backup & Recovery Strategy:
Blob Storage:
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 (blob 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):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| NIS2 Directive compliance (EU critical infrastructure) | MEDIUM | CRITICAL | - Energy sector falls under NIS2 - Incident reporting procedures (24h to authorities) - Security measures documentation - Annual security audit - CISO designated | Legal/Compliance |
| Swedish Säkerhetspolisen (SÄPO) requirements | LOW | HIGH | - Enhanced security for critical infrastructure - Incident reporting to MSB (Swedish Civil Contingencies) - Employee background checks for production access - Security clearance for key personnel | Security Officer |
| API key theft/leakage | MEDIUM | HIGH | - Rotate keys every 90 days - Monitor for leaked keys (GitHub scanning) - Revoke compromised keys immediately - API key hashing in database - Never log full API keys | Security Officer |
| Insider threat (privileged access abuse) | LOW | CRITICAL | - Least privilege principle - All actions audited - Regular access reviews - Separation of duties - Anomaly detection in audit logs | Security Officer |
| Third-party vendor breach (SendGrid, 21G) | LOW | HIGH | - Data Processing Agreements (DPAs) signed - Regular vendor security assessments - Minimal data sharing - Encryption in transit to vendors - Vendor breach response plan | Legal/Compliance |
4.5 NFR-005: Data Retention & Lifecycle Management
Requirement: The system shall manage data retention according to Swedish accounting law (7-year invoice retention) with automated lifecycle policies for cost optimization.
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 Day 366-2555: Cool Day 2556+: Archive | Permanent deletion after 7 years |
| Batch Source Files (XML) | None (internal processing) | 90 days | Day 0-30: Hot Day 31-90: Cool Day 91+: Delete | Automatic deletion |
| Batch Metadata JSON | Audit trail | 90 days | Day 0-90: Hot Day 91+: Delete | Automatic deletion |
| Audit Logs (PostgreSQL) | GDPR, Swedish law | 7 years | Year 0-1: PostgreSQL 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) Cool (archived) | Never deleted |
| Organization Config | Business continuity | Indefinite | Hot | Never deleted (updated in place) |
Azure Blob Lifecycle Policy:
{
"rules": [
{
"enabled": true,
"name": "invoice-lifecycle",
"type": "Lifecycle",
"definition": {
"filters": {
"blobTypes": ["blockBlob"],
"prefixMatch": ["invoices-"]
},
"actions": {
"baseBlob": {
"tierToCool": {
"daysAfterModificationGreaterThan": 365
},
"tierToArchive": {
"daysAfterModificationGreaterThan": 2555
},
"delete": {
"daysAfterModificationGreaterThan": 2920
}
}
}
}
},
{
"enabled": true,
"name": "batch-source-cleanup",
"type": "Lifecycle",
"definition": {
"filters": {
"blobTypes": ["blockBlob"],
"prefixMatch": ["batches-"]
},
"actions": {
"baseBlob": {
"tierToCool": {
"daysAfterModificationGreaterThan": 30
},
"delete": {
"daysAfterModificationGreaterThan": 90
}
}
}
}
}
]
}
Storage Growth Projection:
Assumptions:
- 10M invoices/month
- 85KB per invoice (50KB PDF + 30KB HTML + 5KB JSON)
- 7-year retention
Growth Over Time:
| Year | New Data/Month | Cumulative Total | Primary Storage Tier | Secondary Tier |
|---|---|---|---|---|
| Year 1 | 850 GB | 10.2 TB | Hot (10.2 TB) | - |
| Year 2 | 850 GB | 20.4 TB | Hot (10.2 TB) | Cool (10.2 TB) |
| Year 3 | 850 GB | 30.6 TB | Hot (10.2 TB) | Cool (20.4 TB) |
| Year 7 | 850 GB | 71.4 TB | Hot (10.2 TB) | Cool (10.2 TB), Archive (51 TB) |
Storage Tier Pricing Impact:
With lifecycle policies (Hot → Cool → Archive):
- Year 1-2: Manageable with hot storage
- Year 3-7: Significant savings with tiering (estimated 85% reduction vs all-hot)
Acceptance Criteria:
| Criterion | Validation Method | Target |
|---|---|---|
| Automated lifecycle policies configured | Check Azure policy | Policies active |
| Data transitions to Cool after 1 year | Verify tier of 13-month-old invoice | Cool tier |
| Data transitions to Archive after 7 years | Verify tier of 7-year-old invoice | Archive tier |
| 7-year invoice retention enforced | Attempt to access 8-year-old invoice | Deleted (404) |
| Old batch files deleted after 90 days | Check for 91-day-old batch file | Deleted (404) |
| Retention policy exceptions supported | Tag invoice with legal hold | Not deleted despite age |
| Legal hold prevents deletion | Set legal hold, verify no deletion | Invoice retained |
| Data restoration from Archive within 24h | Request archived invoice | Retrieved within 24h |
| Templates never automatically deleted | Check template age | Old templates present (archived) |
Legal Hold Functionality:
{
"invoiceId": "uuid",
"legalHold": {
"enabled": true,
"reason": "Customer dispute - case #12345",
"appliedBy": "user-uuid",
"appliedAt": "2025-11-21T10:00:00Z",
"expiresAt": null
}
}
Dependencies:
- Azure Blob lifecycle management
- Legal hold tagging mechanism
- Retention compliance monitoring
- Archive tier data retrieval procedures
Risks & Mitigation (Swedish Legal Context):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Bokföringslagen (Accounting Act) non-compliance | LOW | CRITICAL | - 7-year retention strictly enforced - Legal opinion obtained - Retention policy reviewed by auditor - Automated compliance reporting - Skatteverket (Tax Agency) audit trail | Legal/Compliance |
| Premature invoice deletion | LOW | HIGH | - Lifecycle policy testing in staging - Deletion logging and alerts - Soft delete (7-day recovery) - Annual retention audit | Operations Manager |
| Storage costs exceed budget | MEDIUM | MEDIUM | - Lifecycle policies reduce costs 85% - Cost monitoring and alerts - Quarterly cost review - Consider compression for PDFs | Finance Controller |
| Archive retrieval SLA breach | LOW | MEDIUM | - Document 24-hour SLA for archive - Test archive retrieval monthly - Maintain critical invoices in Cool (not Archive) | Operations Manager |
4.6 NFR-006: Monitoring, Logging & Observability
Requirement: The system shall provide comprehensive monitoring through Application Insights, structured logging with Serilog, real-time dashboards with 5-minute refresh, and automated alerting with escalation procedures.
Priority: HIGH
4.6.1 Structured Logging Standards
Serilog Configuration:
Log.Logger = new LoggerConfiguration() .MinimumLevel.Information() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) .Enrich.FromLogContext() .Enrich.WithProperty("Application", "EGU.Flow") .Enrich.WithProperty("Environment", environment) .Enrich.WithProperty("Region", azureRegion) .Enrich.WithMachineName() .Enrich.WithThreadId() .WriteTo.ApplicationInsights( connectionString, TelemetryConverter.Traces, LogEventLevel.Information) .WriteTo.Console(new CompactJsonFormatter()) .CreateLogger();
Log Entry Structure:
{
"timestamp": "2025-11-21T10:30:00.123Z",
"level": "Information",
"messageTemplate": "Batch {BatchId} processing started for organization {OrganizationId}. Items: {ItemCount}",
"message": "Batch 550e8400... processing started for organization 123e4567.... Items: 5000",
"properties": {
"BatchId": "550e8400-e29b-41d4-a716-446655440000",
"OrganizationId": "123e4567-e89b-12d3-a456-426614174000",
"ItemCount": 5000,
"CorrelationId": "corr-abc-123",
"Application": "EGU.Flow",
"Environment": "Production",
"Region": "westeurope",
"MachineName": "worker-001"
}
}
PII Masking Rules:
NEVER log:
- Personnummer (Swedish social security numbers)
- Full customer names (log customer ID only)
- Email addresses (log domain only: user@***@example.se)
- Phone numbers (log prefix only: +4670****)
- Street addresses (log city only)
- Bank account numbers
- API keys or tokens
SAFE to log:
- Organization IDs (UUIDs)
- Batch IDs (UUIDs)
- Invoice IDs (UUIDs)
- Invoice numbers (business references)
- Processing statistics
- Error codes and messages
- Performance metrics
4.6.2 Application Insights Dashboards
Dashboard 1: Operations (Real-Time)
Refresh: Every 5 minutes
Metrics:
- Active batches count (gauge)
- Queue depths (4 queues, time series chart)
- Worker instance counts per service (bar chart)
- Processing throughput (items/minute, time series)
- Error rate (percentage, last hour vs last 24h)
- System health status (green/yellow/red indicators)
- Current API request rate (RPS)
Dashboard 2: Performance
Refresh: Every 5 minutes
Metrics:
- API response times (p50, p95, p99 - line chart)
- Batch processing duration (histogram)
- PDF generation times (p50, p95, p99)
- Handlebars rendering times (p50, p95, p99)
- Delivery latency by channel (email vs postal)
- Worker CPU/memory utilization
- Database query performance (slow query tracking)
Dashboard 3: Business
Refresh: Hourly
Metrics:
- Invoices processed (today, this week, this month - counters)
- Delivery channel breakdown (pie chart: email/postal)
- Failed deliveries by reason (bar chart)
- Top 10 organizations by volume (bar chart)
- Processing trends (daily invoice count, 30-day chart)
- Monthly invoice volumes (seasonal view)
Dashboard 4: Vendor Formats
Refresh: Hourly
Metrics:
- Batches by vendor format (pie chart: GASEL/XELLENT/ZYNERGY)
- Parsing success rate by vendor (percentage gauges)
- Average batch size by vendor
- Parsing duration by vendor
- Validation errors by vendor format
4.6.3 Alert Rules & Escalation
Critical Alerts (5-minute evaluation window):
| Alert Name | Condition (Kusto Query) | Severity | Recipients | Escalation (after 15 min) |
|---|---|---|---|---|
| High Error Rate | traces | where severityLevel >= 3 | count > 50 | High | Ops team | Dev team + On-call |
| Queue Depth Critical | customMetrics | where name == 'Queue.Depth' and value > 10000 | High | Ops team | Product Owner |
| Worker Crash Spike | traces | where message contains 'Worker crashed' | count > 3 | Critical | Ops + Dev teams | CTO |
| Delivery Failure Rate | customMetrics | where name startswith 'Delivery' and value < 0.9 | Medium | Ops team | Customer success |
| API Response Degraded | requests | summarize p95=percentile(duration, 95) | where p95 > 1000 | Medium | Ops team | Technical Architect |
| Batch Processing Timeout | customMetrics | where name == 'Batch.Duration' and value > 120 | High | Ops team | Product Owner |
| Database Connection Errors | exceptions | where type contains 'Npgsql' | count > 10 | Critical | Ops + DBA | CTO |
| Blob Storage Throttling | exceptions | where message contains '503 Server Busy' | count > 20 | High | Ops team | Technical Architect |
| SendGrid Deliverability Drop | SendGrid webhook: bounceRate > 10% | High | Ops team | Email deliverability specialist |
| 21G SFTP Connection Failure | SFTP connection exceptions | High | Ops team | 21G account manager |
Alert Delivery:
- Primary: Email to ops team distribution list
- Secondary: SMS to on-call engineer
- Escalation: PagerDuty incident creation
- Integration: Create Jira ticket automatically
On-Call Rotation:
- European team: 24/7 coverage (Swedish, Danish time zones)
- Shift schedule: Week-long rotations
- Handoff procedure: Thursday 09:00 CET
Acceptance Criteria:
| Criterion | Validation Method | Target |
|---|---|---|
| Dashboards refresh every 5 minutes | Check dashboard timestamp | ≤ 5 min old |
| Data retained for 90 days | Check oldest data in dashboard | 90 days accessible |
| Dashboards accessible to authorized users | Login as different roles | Appropriate access |
| Critical alerts trigger within 5 min | Simulate high error rate | Alert within 5 min |
| Alert escalation after 15 min | Don't acknowledge alert | Escalation triggered |
| PII masked in logs | Search logs for personnummer regex | Zero matches |
| Correlation IDs trace requests | Follow request across services | Same ID throughout |
| Log retention 90 days | Check Application Insights retention | 90 days |
| Structured logging in JSON format | Parse log entries | Valid JSON |
Custom Metrics Tracked:
public class MetricsService { private readonly TelemetryClient _telemetry; public void TrackBatchProcessing(BatchMetadata batch) { _telemetry.TrackMetric("Batch.TotalItems", batch.Statistics.TotalItems, new Dictionary<string, string> { ["OrganizationId"] = batch.OrganizationId, ["VendorCode"] = batch.VendorInfo.VendorCode }); _telemetry.TrackMetric("Batch.Duration", (batch.Timestamps.CompletedAt - batch.Timestamps.StartedAt)?.TotalMinutes ?? 0); _telemetry.TrackMetric("Batch.SuccessRate", (double)batch.Statistics.SuccessfulItems / batch.Statistics.TotalItems * 100); } public void TrackDelivery(string channel, bool success, string organizationId) { _telemetry.TrackMetric($"Delivery.{channel}.Success", success ? 1 : 0, new Dictionary<string, string> { ["OrganizationId"] = organizationId, ["Channel"] = channel }); } public void TrackQueueDepth(string queueName, int depth) { _telemetry.TrackMetric("Queue.Depth", depth, new Dictionary<string, string> { ["QueueName"] = queueName }); } public void TrackVendorParsing(string vendorCode, bool success, long durationMs) { _telemetry.TrackMetric("Parser.Duration", durationMs, new Dictionary<string, string> { ["VendorCode"] = vendorCode, ["Success"] = success.ToString() }); } }
Kusto Queries for Common Operations:
Query 1: Failed Batches (Last 24 Hours)
traces | where timestamp > ago(24h) | where customDimensions.BatchId != "" | where severityLevel >= 3 | summarize ErrorCount = count(), FirstError = min(timestamp), LastError = max(timestamp) by BatchId = tostring(customDimensions.BatchId), OrganizationId = tostring(customDimensions.OrganizationId), ErrorMessage = message | order by ErrorCount desc | take 50
Query 2: Queue Depth Trending
customMetrics | where name == "Queue.Depth" | where timestamp > ago(24h) | extend QueueName = tostring(customDimensions.QueueName) | summarize AvgDepth = avg(value), MaxDepth = max(value), MinDepth = min(value) by QueueName, bin(timestamp, 5m) | render timechart
Query 3: Vendor Format Performance
customMetrics | where name == "Parser.Duration" | where timestamp > ago(7d) | extend VendorCode = tostring(customDimensions.VendorCode) | summarize p50 = percentile(value, 50), p95 = percentile(value, 95), p99 = percentile(value, 99), BatchCount = count() by VendorCode | order by p95 desc
Dependencies:
- Application Insights workspace (90-day retention)
- Serilog sinks for Application Insights and Console
- Alert action groups configured
- Dashboard permissions configured
- PagerDuty or similar on-call system
4.7 NFR-007: Disaster Recovery & Business Continuity
Requirement: The system shall have documented disaster recovery procedures with defined RTO/RPO targets, tested quarterly, to ensure business continuity for Nordic utility customers.
Priority: HIGH
Disaster Recovery Scenarios:
Scenario 1: Worker Instance Crash
Trigger: DocumentGenerator worker crashes during 32-item batch processing
Detection: Worker stops sending heartbeats, Azure Container Apps detects unhealthy instance
Recovery Procedure:
1. Azure Container Apps automatically starts new instance (2 min)
2. Queue message visibility timeout expires (5 min)
3. Message becomes visible in batch-items-queue again
4. New worker instance picks up message
5. Worker checks for already-processed invoices (idempotency)
6. Skips completed invoices, processes remaining items
7. Blob lease ensures no concurrent processing
RTO: < 5 minutes (automatic)
RPO: 0 (no data loss, idempotent operations)
Responsible: Automatic + Operations Manager (monitoring)
Scenario 2: PostgreSQL Database Failure
Trigger: Primary PostgreSQL instance becomes unresponsive
Detection: Health check failures, connection timeout errors in logs
Recovery Procedure:
1. Azure detects primary failure via health probes (30 seconds)
2. Auto-failover to read replica in secondary region (5 min)
3. DNS updated to point to new primary
4. Application reconnects automatically (connection retry logic)
5. Verify data integrity post-failover
6. Notify stakeholders of failover event
7. Investigate root cause
RTO: < 15 minutes
RPO: < 5 minutes (replication lag)
Responsible: Operations Manager (monitoring), DBA (verification)
Scenario 3: Azure Region Failure (West Europe)
Trigger: Complete West Europe region outage
Detection: Traffic Manager health checks fail for all West Europe endpoints
Recovery Procedure:
1. Traffic Manager detects 3 consecutive health check failures (90 seconds)
2. Traffic Manager routes all traffic to North Europe (2 min)
3. North Europe region activates read replica database as primary
4. North Europe workers process queues (messages replicated via GRS)
5. Verify system operational in North Europe
6. Communicate to customers about region failover
7. Monitor for West Europe recovery
8. Plan failback when West Europe restored
RTO: < 30 minutes
RPO: < 15 minutes (blob replication lag)
Responsible: Operations Manager (execution), CTO (decision), Communications (customer notification)
Scenario 4: Blob Storage Corruption
Trigger: Critical blob (organization config, template) becomes corrupted or accidentally deleted
Detection: Blob read errors, validation failures, user reports
Recovery Procedure:
1. Identify corrupted blob path and organization
2. Check soft delete (7-day retention):
- If within 7 days: Undelete blob immediately
3. If soft delete expired, check blob versions:
- Restore from previous version
4. If no versions, restore from geo-redundant copy:
- Access secondary region blob storage
- Copy to primary region
5. Verify restored blob integrity
6. Test with sample batch
7. Root cause analysis
RTO: < 1 hour
RPO: < 1 hour (version interval)
Responsible: Operations Manager (execution), Technical Architect (verification)
Scenario 5: Complete Data Loss (Catastrophic)
Trigger: Theoretical scenario - both regions and all backups lost
Detection: N/A (highly unlikely with Azure GRS)
Recovery Procedure:
1. Declare disaster
2. Restore PostgreSQL from geo-redundant backup (35-day retention)
3. Organizations re-upload batch files (source systems have copies)
4. Customers re-notified about invoices
5. Incident post-mortem and Azure investigation
RTO: < 4 hours
RPO: < 24 hours (last backup)
Responsible: CTO (declaration), All teams (execution)
Note: This scenario has probability < 0.001% given Azure GRS + geo-redundant backups + multi-region deployment.
Disaster Recovery Testing:
| Test Type | Frequency | Scope | Pass Criteria |
|---|---|---|---|
| Worker Crash Test | Monthly | Kill random worker mid-processing | Recovery < 5 min, no data loss |
| Database Failover Test | Quarterly | Force failover to replica | Recovery < 15 min, queries work |
| Region Failover Drill | Annually | Simulate West Europe outage | Recovery < 30 min, all services operational |
| Backup Restoration Test | Monthly | Restore PostgreSQL from backup | Successful restore, data integrity verified |
| Blob Undelete Test | Quarterly | Delete critical blob, restore | Successful recovery within 1 hour |
Acceptance Criteria:
| Criterion | Validation Method | Target |
|---|---|---|
| Multi-region deployment active | Verify services in both regions | Both regions operational |
| Traffic Manager failover tested | Simulate region failure | Failover < 2 min |
| Database auto-failover tested | Force primary DB failure | Failover < 15 min |
| Blob geo-replication verified | Write to primary, read from secondary | Data present |
| Disaster recovery procedures documented | Review runbook completeness | 100% complete |
| DR drill conducted quarterly | Check last drill date | Within 90 days |
| Backup restoration tested monthly | Check last restore test | Within 30 days |
| Recovery procedures automated where possible | Review manual steps | < 5 manual steps |
4.8 NFR-008: Data Consistency (Eventual Consistency Model)
Requirement: The system shall maintain data consistency using blob leases for exclusive access, ETag-based optimistic concurrency for metadata updates, and idempotent operations for safe retries.
Priority: HIGH
Consistency Guarantees:
Strong Consistency (Within Single Operation):
- Blob lease acquisition: One worker per 32-item batch
- PostgreSQL transactions: User/role operations ACID
- Queue message delivery: At-least-once delivery
Eventual Consistency (Across System):
- Batch statistics: Updated within 30 seconds
- Invoice status: Updated within 1 minute
- Dashboard metrics: Updated within 5 minutes
- Audit logs: Written asynchronously
Consistency Mechanisms:
1. Blob Lease for Exclusive Access:
// Ensures only one worker processes 32-item batch var lease = await AcquireBlobLeaseAsync( container: "{org}-batches-{year}", blob: "locks/{batch-id}/{message-id}.lock", duration: TimeSpan.FromMinutes(5)); try { // Process 32 invoices exclusively await ProcessBatchItemsAsync(message); } finally { await ReleaseBlobLeaseAsync(lease); }
2. ETag-Based Optimistic Concurrency:
// Prevents lost updates to batch metadata var download = await blobClient.DownloadContentAsync(); var etag = download.Value.Details.ETag; var metadata = JsonSerializer.Deserialize<BatchMetadata>(download.Value.Content); // Update metadata metadata.Statistics.ProcessedItems += 32; metadata.Metadata.UpdatedAt = DateTime.UtcNow; metadata.Metadata.Version++; // Upload with ETag condition await blobClient.UploadAsync(content, new BlobUploadOptions { Conditions = new BlobRequestConditions { IfMatch = etag } }); // Throws if ETag doesn't match (another worker updated)
3. Idempotent Operations:
// Safe to retry without duplicates public async Task ProcessInvoiceAsync(string invoiceId) { // Check if already processed var metadata = await TryGetInvoiceMetadataAsync(invoiceId); if (metadata?.Status == "delivered") { _logger.LogInformation("Invoice {InvoiceId} already processed, skipping", invoiceId); return; // Idempotent: safe to skip } // Process invoice (creates new blobs, doesn't update existing) await RenderInvoiceAsync(invoiceId); await GeneratePdfAsync(invoiceId); await EnqueueForDeliveryAsync(invoiceId); }
Acceptance Criteria:
| Criterion | Validation Method | Target |
|---|---|---|
| Blob leases prevent concurrent processing | Send same message to 2 workers | Only 1 processes |
| ETags prevent lost updates | 2 workers update same metadata | No lost updates |
| Retry operations are idempotent | Retry invoice processing 3x | Processed once only |
| No duplicate invoices generated | Crash during processing, retry | One PDF created |
| Concurrent batch updates handled | 10 workers update statistics | All updates applied |
| Race conditions prevented | Concurrent access testing | No race conditions |
| Data integrity after crash | Kill worker, verify data state | Consistent state |
Dependencies:
- Azure Blob Storage lease API
- ETag support in blob operations
- Retry logic with idempotency checks
- Worker coordination mechanisms
4.9 NFR-009: Multi-Region Data Residency (Nordic Compliance)
Requirement: The system shall enforce data residency requirements for Nordic countries, ensuring Swedish/Danish data stays in West Europe and Norwegian/Finnish data stays in North Europe, with no cross-border transfers except encrypted backups.
Priority: HIGH
Data Residency Rules:
| Country | Customer Base | Primary Region | Data Residency | Backup Region | Rationale |
|---|---|---|---|---|---|
| Sweden (SE) | ~10M population, largest market | West Europe | Enforced | North Europe (encrypted) | GDPR, Swedish Data Protection Law |
| Denmark (DK) | ~6M population | West Europe | Enforced | North Europe (encrypted) | GDPR, Danish data laws |
| Norway (NO) | ~5M population | North Europe | Enforced | West Europe (encrypted) | GDPR, Norwegian data laws, EEA regulations |
| Finland (FI) | ~5M population | North Europe | Enforced | West Europe (encrypted) | GDPR, Finnish data laws |
Traffic Routing Logic:
public class RegionRoutingService { public string GetProcessingRegion(string organizationId) { var org = await _orgService.GetOrganizationAsync(organizationId); // Route based on organization's country return org.CountryCode switch { "SE" => "westeurope", // Sweden → West Europe "DK" => "westeurope", // Denmark → West Europe "NO" => "northeurope", // Norway → North Europe "FI" => "northeurope", // Finland → North Europe _ => "westeurope" // Default: West Europe }; } public async Task<bool> ValidateDataResidencyAsync(string organizationId, string requestRegion) { var requiredRegion = GetProcessingRegion(organizationId); if (requestRegion != requiredRegion) { _logger.LogWarning( "Data residency violation attempted. Org: {OrgId}, Required: {Required}, Attempted: {Attempted}", organizationId, requiredRegion, requestRegion); return false; } return true; } }
Organization Configuration:
{
"organizationId": "uuid",
"organizationCode": "VATTENFALL-SE",
"countryCode": "SE",
"dataResidency": {
"primaryRegion": "westeurope",
"allowedRegions": ["westeurope"],
"backupRegions": ["northeurope"],
"crossRegionProcessing": false,
"crossRegionBackup": true
}
}
Acceptance Criteria:
| Criterion | Validation Method | Target |
|---|---|---|
| Swedish orgs process in West Europe | Verify blob container region | westeurope |
| Norwegian orgs process in North Europe | Verify blob container region | northeurope |
| Cross-region processing blocked | Attempt to process Swedish org in North Europe | Rejected |
| Cross-region backup allowed | Verify geo-redundant replication | Enabled |
| Latency < 100ms within region | API latency from Nordic countries | < 100ms |
| Automatic failover to secondary region | Simulate primary region failure | Failover works |
| Data residency config per organization | Update org config | Setting honored |
| Audit trail for cross-region access | Attempt cross-region, check logs | Attempt logged |
Dependencies:
- Azure Traffic Manager with geographic routing
- Blob storage geo-redundant replication
- Organization configuration enforcement
- Multi-region deployment automation
Risks & Mitigation (Nordic Legal Context):
| Risk | Likelihood | Impact | Mitigation Strategy | Owner |
|---|---|---|---|---|
| Schrems II implications (EU-US data transfer) | LOW | HIGH | - No US region usage - All data in EU (West/North Europe only) - Azure EU Data Boundary compliance - Standard Contractual Clauses with vendors | Legal/Compliance |
| Norwegian data sovereignty concerns | LOW | MEDIUM | - North Europe primary for Norwegian orgs - Option for Norway-only processing - No data transfer to other Nordics without consent - Compliance with Norwegian regulations | Legal/Compliance |
| Data residency audit by Datatilsynet (NO) or Datatilsynet (DK) | LOW | HIGH | - Documented data flows - Data residency configuration auditable - Logs prove compliance - Annual self-assessment | Legal/Compliance |
4.10 NFR-010: Maintainability & Code Quality
Requirement: The system shall be designed for long-term maintainability with clear code standards, comprehensive documentation, high test coverage, and adherence to .NET best practices.
Priority: MEDIUM
Code Quality Standards:
Project Structure (Updated per Oct 27 decision):
EGU.Flow/
├── EGU.Flow.AppHost/ # .NET Aspire orchestration
│ └── Program.cs
│
├── EGU.Flow.Core/ # Shared contracts, DTOs, interfaces
│ ├── DTOs/
│ │ ├── BatchUploadRequest.cs
│ │ ├── CanonicalInvoice.cs
│ │ └── DeliveryRequest.cs
│ ├── Enums/
│ │ ├── BatchStatus.cs
│ │ ├── DeliveryChannel.cs
│ │ └── VendorCode.cs
│ ├── Interfaces/
│ │ ├── IXmlParserService.cs
│ │ ├── ITemplateRenderingService.cs
│ │ └── IDeliveryService.cs
│ └── Common/
│ ├── Constants.cs
│ └── ErrorCodes.cs
│
├── EGU.Flow.Domain/ # Domain models and business logic
│ ├── Models/
│ │ ├── Organization.cs
│ │ ├── Batch.cs
│ │ ├── BatchItem.cs
│ │ ├── Template.cs
│ │ └── TemplateCategory.cs
│ └── DomainServices/
│ ├── VendorDetectionService.cs
│ └── DistributionRoutingService.cs
│
├── EGU.Flow.BusinessLogic/ # Business services
│ ├── Services/
│ │ ├── BatchService.cs
│ │ ├── OrganizationService.cs
│ │ ├── TemplateService.cs
│ │ └── SchemaRegistryService.cs
│ └── Mappers/
│ ├── GaselMapper.cs
│ ├── XellentMapper.cs
│ └── ZynergyMapper.cs
│
├── EGU.Flow.CoreApiService/ # ASP.NET Core REST API
│ ├── Controllers/
│ │ ├── BatchController.cs
│ │ ├── OrganizationController.cs
│ │ ├── TemplateController.cs
│ │ └── SchemaController.cs
│ ├── Middleware/
│ │ ├── OrganizationContextMiddleware.cs
│ │ ├── ErrorHandlingMiddleware.cs
│ │ └── RequestLoggingMiddleware.cs
│ └── Program.cs
│
├── EGU.Flow.ParserService/ # Console app: XML → JSON
│ ├── Workers/
│ │ └── BatchParserWorker.cs
│ ├── Parsers/
│ │ ├── GaselParser.cs
│ │ ├── XellentParser.cs
│ │ └── ZynergyParser.cs
│ └── Program.cs
│
├── EGU.Flow.DocumentGenerator/ # Console app: JSON → PDF
│ ├── Workers/
│ │ └── DocumentGeneratorWorker.cs
│ ├── Services/
│ │ ├── HandlebarsRenderingService.cs
│ │ └── PlaywrightPdfService.cs
│ └── Program.cs
│
├── EGU.Flow.EmailService/ # Console app: Email delivery
│ ├── Workers/
│ │ └── EmailDeliveryWorker.cs
│ └── Program.cs
│
├── EGU.Flow.PostalService/ # Console app: 21G integration
│ ├── Workers/
│ │ └── PostalBulkProcessor.cs
│ ├── Services/
│ │ ├── SftpService.cs
│ │ └── ZipArchiveService.cs
│ └── Program.cs
│
├── EGU.Flow.Web/ # Future: Blazor UI
│ └── (Phase 2)
│
└── EGU.Flow.Tests/ # Test projects
├── EGU.Flow.UnitTests/
├── EGU.Flow.IntegrationTests/
└── EGU.Flow.LoadTests/
Coding Standards:
- Style: Follow Microsoft C# coding conventions
- Naming: PascalCase for public members, camelCase for private
- Comments: XML documentation on all public APIs
- Async: Always use async/await, never .Result or .Wait()
- Logging: Structured logging with Serilog, correlation IDs
- Exceptions: Custom exception types, never swallow exceptions
- Null safety: Use nullable reference types (C# 12)
Acceptance Criteria:
| Criterion | Validation Method | Target |
|---|---|---|
| Code follows .NET conventions | EditorConfig + Roslyn analyzers | Zero warnings |
| XML documentation on public APIs | Documentation coverage report | 100% of public members |
| Unit test coverage | Code coverage report | > 70% |
| Integration test coverage | Test execution report | > 25% of critical paths |
| Swagger/OpenAPI for all endpoints | Verify Swagger UI | All endpoints documented |
| Correlation IDs in all logs | Trace request through system | Same ID across services |
| Health check endpoints present | GET /health on all services | All respond |
| Feature flags for gradual rollout | Verify feature flag configuration | Flags configurable |
| Database migration scripts versioned | Check migrations folder | Sequential numbering |
| Infrastructure as Code (Bicep) | All resources in Bicep templates | 100% infrastructure |
| No hardcoded values | Code scan | All config in appsettings/KeyVault |
Documentation Requirements:
| Document Type | Location | Update Frequency | Owner |
|---|---|---|---|
| API Documentation | Swagger UI at /swagger | Every API change | Dev Team |
| Architecture Decisions | Confluence ADR page | Per decision | Technical Architect |
| Deployment Procedures | Confluence + Git (docs/) | Per change | Ops Team |
| Operations Runbook | Confluence | Monthly review | Ops Manager |
| Disaster Recovery Plan | Confluence (restricted) | Quarterly | Ops Manager |
| GDPR Documentation | Confluence (restricted) | Annual review | Legal/Compliance |
| Test Data Generation Guide | Git (docs/) | Per update | QA Team |
Dependencies:
- EditorConfig file in repository
- Roslyn analyzer NuGet packages
- SonarQube or similar code quality tool
- Documentation review in PR process
4.11 NFR-011: Usability & Developer Experience
Requirement: The API shall be intuitive, well-documented, easy to integrate, with clear error messages, code samples, and Postman collections.
Priority: MEDIUM
API Design Principles:
RESTful Standards:
- Resource-based URLs:
/organizations/{id}/batches - HTTP verbs: GET (read), POST (create), PUT (update), DELETE (not used Phase 1)
- Status codes: 200 OK, 201 Created, 202 Accepted, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests, 500 Internal Error, 503 Service Unavailable
- Consistent JSON response envelope
- Pagination for list endpoints
- Filtering and sorting support
Error Message Quality:
Bad Example:
{
"error": "Invalid input"
}
Good Example:
{
"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": "Check that all opening tags have matching closing tags in the correct order",
"documentationUrl": "https://docs.egflow.com/errors/INVALID_XML"
}
}
]
}
Acceptance Criteria:
| Criterion | Validation Method | Target |
|---|---|---|
| RESTful design principles followed | API design review | All principles followed |
| Consistent request/response structure | Review all endpoints | Same envelope |
| Error messages include suggestions | Test error scenarios | Actionable guidance |
| Error messages include documentation links | Check error response | URLs present |
| Line/column numbers for XML errors | Upload invalid XML | Position info present |
| Comprehensive Swagger documentation | Review Swagger UI | All endpoints, examples |
| Code samples for common operations | Check documentation | C#, curl examples |
| Postman collection available | Import collection, run requests | All requests work |
| API versioning clear | Check URL structure | /v1/ in all paths |
| Deprecation warnings 6 months advance | Deprecate endpoint | Warning in response |
Postman Collection Contents:
EG Flow API Collection/
├── Authentication/
│ └── Get Access Token
├── Batch Management/
│ ├── Upload Batch
│ ├── Start Batch Processing
│ ├── Get Batch Status
│ ├── List Batch Items
│ └── Get Batch Item Details
├── Organization/
│ ├── Get Organization
│ └── Update Organization Config
├── Templates/
│ ├── List Templates
│ ├── Get Template
│ └── List Template Categories
└── Schema Management/
├── List Supported Formats
└── Validate XML
Dependencies:
- OpenAPI/Swagger generator
- Postman collection export
- Documentation website/portal
- Code sample generation
4.12 NFR-012: Internationalization (Nordic Languages)
Requirement: The system shall support Swedish language for invoices, email notifications, error messages, and UI elements, with foundation for future Norwegian, Danish, and Finnish support.
Priority: MEDIUM
Localization Requirements:
Phase 1: Swedish Only
- Invoice templates: Swedish text
- Email notifications: Swedish
- Error messages: Swedish (with English fallback in details)
- Date formats: YYYY-MM-DD (ISO 8601, universal)
- Number formats: Space as thousand separator, comma as decimal (Swedish standard)
- Example: 1 234 567,89 kr
- Currency: SEK (Swedish Krona)
- Time zone: CET/CEST (Europe/Stockholm)
Phase 2: Multi-Language (Future)
- Norwegian (Bokmål): For Norwegian utilities
- Danish: For Danish utilities
- Finnish: For Finnish utilities
- Language detection based on organization country
Swedish-Specific Formatting:
Dates:
- Invoice date: "2025-11-21" (ISO format, universal)
- Display: "21 november 2025" (Swedish long format)
Numbers:
- Amount: 1 234,56 (space separator, comma decimal)
- Percentage: 25,0% (comma decimal)
- Quantity: 1 234 (integer, space separator)
Currency:
- Symbol: "kr" or "SEK"
- Position: After amount "1 234,56 kr"
Swedish Terms:
- Faktura (Invoice)
- Förfallodatum (Due date)
- Mätpunkt (Metering point)
- Elförbrukning (Electricity consumption)
- Att betala (Amount to pay)
- Moms (VAT)
Handlebars Helpers for Swedish Formatting:
{{!-- Swedish number formatting --}} {{formatNumber amount decimals=2}} → "1 234,56" {{!-- Swedish currency --}} {{formatCurrency amount}} → "1 234,56 kr" {{!-- Swedish date --}} {{formatDate date format="long"}} → "21 november 2025" {{!-- Swedish percentage --}} {{formatPercent rate}} → "25,0%"
Acceptance Criteria:
| Criterion | Validation Method | Target |
|---|---|---|
| Invoice templates in Swedish | Review rendered PDF | Swedish text |
| Email notifications in Swedish | Receive test email | Swedish subject/body |
| Error messages in Swedish | Trigger various errors | Swedish messages |
| Numbers formatted Swedish style | Check invoice amounts | "1 234,56" format |
| Dates in ISO 8601 | Check invoice JSON | "2025-11-21" format |
| Currency symbol positioned correctly | Check rendered invoice | "kr" after amount |
| Swedish characters (åäö) render correctly | PDF with åäö | Characters correct |
| Time zone CET/CEST used | Check timestamps | Europe/Stockholm |
Dependencies:
- Swedish localization resources
- Handlebars custom helpers
- PDF font with Swedish character support
- Future: i18n library for multi-language
4.13 NFR-013: API Backward Compatibility
Requirement: API versions shall maintain backward compatibility for minimum 12 months after new version release, with clear deprecation warnings and migration guides.
Priority: HIGH
Versioning Strategy:
URL Path Versioning:
Current: https://api.egflow.com/v1/organizations/{id}/batches
Future: https://api.egflow.com/v2/organizations/{id}/batches
Both versions run simultaneously during transition period
Version Lifecycle:
v1.0 Released: January 2026
↓
v2.0 Released: January 2027
↓ (v1 deprecation announced)
v1 Supported: Until January 2028 (12 months)
↓
v1 Sunset: January 2028
Deprecation Warning (HTTP Header):
HTTP/1.1 200 OK Deprecation: true Sunset: Sat, 31 Jan 2028 23:59:59 GMT Link: <https://docs.egflow.com/api/v2/migration>; rel="successor-version"
Acceptance Criteria:
| Criterion | Validation Method | Target |
|---|---|---|
| v1 supported 12 months after v2 release | Verify both versions work | Both return 200 |
| Breaking changes only in major versions | Review v1.1, v1.2 changes | No breaking changes |
| Deprecation warnings 6 months advance | Check headers 6 months before sunset | Deprecation header present |
| Migration guide published | Review documentation | Complete guide available |
| v1 clients continue working during v2 rollout | Test v1 client after v2 deploy | No disruption |
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):
| 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 |