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.
...
❌ Kivra digital mailbox integration (Phase 2)
❌ e-Faktura/PEPPOL electronic invoicing (Phase 2)
❌ SMS distribution via Wiraya (Phase 2)
❌ Customer self-service portal (Phase 2)
❌ Payment processing
1.3 Success Metrics
| Category | Metric | Target | Measurement Method |
|---|---|---|---|
| Processing Efficiency | Batch processing time (100K invoices) | < 2 hours | Timestamp tracking in metadata |
| System Performance | API response time (p95) | < 500ms | Application Insights |
| Reliability | System uptime | 99.9% | Azure Monitor |
| Quality | Batch success rate | > 99.5% | (completed/total) × 100 |
| Delivery | Email delivery rate | > 95% | SendGrid analytics |
| Delivery | Postal delivery success | > 98% | 21G confirmation tracking |
| Scale | Monthly processing capacity | 10M+ invoices | System throughput monitoring |
| User Satisfaction | Customer satisfaction score | > 4.5/5 | Quarterly survey |
| Support | Support ticket volume | < 50/month | Jira ticket tracking |
...
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
...
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
...
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
...
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
...
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
...
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
...
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
...
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
...
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
...
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
...
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)
...
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
...
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
...
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
...
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
...
{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
...
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)
...
{
"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:
...
- 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)
...
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:
...
- 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
...
<!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
}
}
}
}...
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)
...
<?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:
...
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
...
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)
...
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
...
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
}
}...
{
"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
...
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)
...
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
...
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
...
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: 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
...
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):
...
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):
...
- 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)
...
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:
...
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
...
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
...
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:
...
- 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:
...
- 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:
...
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
...
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
...
- 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() }); } }
...
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)
...
// 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
...
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; } }
...
{
"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
...
- 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
...
{
"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:
...
{{!-- 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
...
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
...
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
...
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
...
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
...
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 |