high priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

The template registry contains exactly 4 admin-specific template types: USER_STATUS_CHANGED, CERTIFICATION_EXPIRY_ALERT, PENDING_REIMBURSEMENT_SUMMARY, EXPORT_COMPLETED
Each template defines: title template string, body template string, notification_type (push/in-app/both), target_role filter (e.g., 'coordinator'), and required_variables list
Template variable substitution is validated at registration time: templates with undefined variables (e.g., `{{userName}}` not in required_variables) throw a `TemplateValidationException` at app startup, not at dispatch time
USER_STATUS_CHANGED template produces localized Norwegian strings by default, with English fallback, using the organization's locale preference
CERTIFICATION_EXPIRY_ALERT includes days-until-expiry as a template variable and displays correctly for both 1-day and 30-day scenarios
PENDING_REIMBURSEMENT_SUMMARY template supports a `count` variable and uses correct Norwegian plural forms (e.g., '1 refusjon' vs '3 refusjoner')
EXPORT_COMPLETED template includes export_type (CSV/Excel), row_count, and download_url as variables
Recipient resolution calls `OrgHierarchyService.getSubtreeIds(orgNodeId)` and queries the users table for users with `role = target_role` within the subtree
The template registry is injected as a dependency — not a singleton with global state — to support testing with mock registries
All templates are stored as Dart const objects — no hardcoded notification strings outside the registry

Technical Requirements

frameworks
Flutter
Dart
Riverpod
apis
Supabase Postgres (user lookup by role and org node)
OrgHierarchyService.getSubtreeIds()
Shared NotificationService (FCM, in-app)
data models
NotificationTemplate
NotificationRecipient
OrgNode
UserProfile
CertificationRecord
ReimbursementRequest
performance requirements
Template lookup is O(1) via enum key — no string scanning or regex at dispatch time
Recipient list resolution for an org subtree of 1400 nodes completes in under 500ms
Template rendering (variable substitution) for a single notification completes in under 5ms
security requirements
Template variable substitution must HTML-escape user-supplied values to prevent injection into notification body (relevant if notifications render HTML)
Recipient resolution enforces org-scope: never returns users outside the specified subtree
Download URLs embedded in EXPORT_COMPLETED notifications must be signed (expiring) Supabase Storage URLs, never permanent public URLs

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Define a `NotificationTemplateType` enum for the 4 admin template types; use it as the registry key for O(1) lookup. Model each template as an immutable Dart class with `const` constructor: `NotificationTemplate({ required this.titleTemplate, required this.bodyTemplate, required this.notificationType, required this.targetRole, required this.requiredVariables })`. Implement variable substitution as a pure function: `String render(String template, Map variables)` using `String.replaceAll()` in a loop over `requiredVariables` — do not use a full regex engine for this. Validate variables at registry construction time by checking that every `{{variable}}` pattern in the template strings has a corresponding entry in `requiredVariables`.

For Norwegian pluralization, implement a minimal `pluralize(int count, String singular, String plural)` helper rather than pulling in an i18n library for two forms. Wrap the shared `NotificationService` with an `AdminNotificationService` facade so admin-specific concerns (template resolution, org-scoped recipients) are separated from the low-level FCM dispatch.

Testing Requirements

Unit tests: instantiate the template registry with all 4 templates and assert no TemplateValidationException is thrown. Test each template's variable substitution with valid inputs and assert the output string matches expected format. Test plural form handling for PENDING_REIMBURSEMENT_SUMMARY with count=1 and count=3. Test that a template with an undefined variable throws TemplateValidationException at registration.

Test recipient resolution: mock OrgHierarchyService and the user query; assert only users with the correct role and within the subtree are returned. Localization test: assert Norwegian strings are returned when org locale is 'nb', English when 'en'. Integration test: register templates, call recipient resolution against a Supabase local instance with seeded users, assert the returned recipient list matches expected user IDs.

Component
Admin Notification Dispatcher
infrastructure medium
Epic Risks (4)
medium impact high prob technical

OrgHierarchyNavigator rendering NHF's full 1,400-chapter tree in a single widget may cause Flutter frame-rate drops below 60 fps on mid-range devices, making the navigator unusable for NHF national admins.

Mitigation & Contingency

Mitigation: Implement lazy expansion: only load immediate children on node expand rather than the full tree upfront. Use virtual scrolling for long sibling lists. Test with a synthetic 1,400-node dataset on a low-end Android device during development.

Contingency: If lazy expansion is insufficient, replace the tree widget with a paginated drill-down navigator (select level → select child) that avoids rendering more than 50 nodes at a time.

medium impact medium prob dependency

Bufdir may update their required export column structure or file format during or after development. If the AdminExportService hardcodes the current Bufdir schema, any format change requires a code release rather than a config update.

Mitigation & Contingency

Mitigation: Drive the Bufdir column mapping from a configuration repository rather than hardcoded constants. Abstract column definitions into a named schema config so that format changes require only a config update and re-deployment without service logic changes.

Contingency: If Bufdir format changes post-launch, release a config update within one sprint. If the change is structural (new required sections), scope a targeted service update and communicate timeline to partner organisations.

high impact medium prob integration

Role transition side-effects in UserManagementService (e.g., certification expiry removing mentor from chapter listing, pause triggering coordinator notification) may interact with external services like HLF's website sync. Incomplete side-effect handling could leave the system in an inconsistent state.

Mitigation & Contingency

Mitigation: Model side-effects as explicit domain events published after the primary state change is persisted. Implement event handlers as idempotent operations so re-processing is safe. Write integration tests that assert all side-effects fire correctly for each role transition type.

Contingency: If a side-effect fails after the primary change is persisted, log the failure with full context and trigger a manual reconciliation alert to the on-call team. Provide an admin-accessible re-trigger action for failed side-effects.

medium impact medium prob scope

If AdminStatisticsService cache TTL is set too long, org_admin may see significantly stale KPI values (e.g., a mentor newly paused an hour ago still appears as active), undermining trust in the dashboard.

Mitigation & Contingency

Mitigation: Default cache TTL to 5 minutes with a manual refresh action on the dashboard. Implement cache invalidation triggered by UserManagementService write operations that affect counted entities.

Contingency: If staleness causes org admin complaints post-launch, reduce TTL to 60 seconds and introduce a real-time Supabase subscription for high-impact counters (paused mentors, expiring certifications).