high priority medium complexity infrastructure pending infrastructure specialist Tier 2

Acceptance Criteria

The ZIP archive directory structure follows the pattern `YYYY-MM/{activity_type_slug}/{attachment_filename}`, grouping files by year-month of the activity date and then by activity type
Filename conflicts within the same directory are resolved by appending a numeric suffix before the extension (e.g. `receipt.pdf`, `receipt_2.pdf`) — no files are silently overwritten
Individual files are streamed from Supabase Storage into the ZIP encoder incrementally — peak memory usage does not exceed 50 MB regardless of total archive size
The completed ZIP archive is uploaded to a private Supabase Storage bucket using a path of `exports/{organisation_id}/{report_period_id}/attachments.zip`
A signed download URL for the uploaded ZIP is returned to the caller with a 24-hour expiry
If any individual file download fails during streaming, that file is skipped, a warning is recorded in the audit log, and archiving continues for remaining files
The ZIP archive is valid and can be opened by standard unzip utilities (Info-ZIP, macOS Archive Utility, Windows Explorer) without errors
An empty manifest input (zero attachments) produces no ZIP file and returns null — callers handle this gracefully
Temporary byte buffers used during streaming are released after each file is added — no memory accumulation across files
The upload to Supabase Storage uses the `upsert: true` option to allow idempotent re-runs if the export is retried
Total ZIP creation and upload duration is logged for a 50-file, 10 MB total payload in under 30 seconds on a standard Edge Function instance

Technical Requirements

frameworks
Flutter
Supabase Dart SDK
archive (pub.dev) package for ZIP encoding
apis
Supabase Storage API (upload, createSignedUrl)
Signed URLs from task-008 manifest for file download
data models
activity
bufdir_export_audit_log
bufdir_column_schema
performance requirements
Peak memory during ZIP creation must not exceed 50 MB for archives up to 500 MB total uncompressed size
ZIP upload to Supabase Storage must use chunked/streaming upload — not a single in-memory byte array for large archives
Individual file streaming must complete within 10 seconds per file; files exceeding this timeout are skipped with a warning
security requirements
ZIP archive stored in a private Supabase Storage bucket — no public access
Signed download URL for the ZIP expires after 24 hours maximum
Archive filename includes the organisation_id to prevent path traversal or cross-organisation access
Temporary files or byte buffers must be disposed after use — no residual data in Edge Function memory after completion
Per-bucket RLS policies must prevent any org from accessing another org's export path

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use the `archive` pub.dev package (`Archive`, `ArchiveFile`, `ZipEncoder`) for ZIP creation. Do NOT load all files into memory first — stream each file by downloading it via its signed URL using `http.get` (or Dart's `HttpClient`) and add the resulting bytes to the archive one file at a time, then release the reference. For the directory structure, build the path string as `'${activity.date.year}-${activity.date.month.toString().padLeft(2,'0')}/${activityTypeSlug}/${resolvedFilename}'`. Conflict resolution: maintain a `Map` of seen paths; on collision, increment counter and reinsert.

For streaming upload to Supabase Storage, use `supabase.storage.from(bucket).uploadBinary(path, bytes, fileOptions: FileOptions(upsert: true))` — for very large archives consider chunking using `upload` with a `Stream` if the SDK supports it. The method signature should be `Future buildAndUploadZip(List manifest, String organisationId, String reportPeriodId)` returning the signed URL or null. Run this method inside a try/catch with an audit log write on failure.

Testing Requirements

Unit tests (flutter_test) must cover: (1) directory structure — given a manifest with activities on two different dates and two types, assert the ZIP contains correctly nested paths; (2) conflict resolution — two attachments with identical filenames in the same directory, assert second is renamed with `_2` suffix; (3) empty manifest — assert method returns null and no upload is attempted; (4) skip on download failure — mock one file download throwing an exception, assert ZIP is created with remaining files and warning is logged; (5) idempotency — calling the method twice with the same report period ID uploads with `upsert: true` and does not throw. Integration test should create a real ZIP from 3 small test files, upload to Supabase test bucket, and verify download via signed URL produces a valid archive. Test coverage for the ZIP packaging method must be at least 85%.

Component
Document Attachment Bundler
service medium
Epic Risks (3)
high impact medium prob technical

NHF contacts can belong to up to five local chapters simultaneously. If the deduplication logic in the activity query service incorrectly attributes cross-chapter activities, organisations will either under-report or over-report to Bufdir, which could trigger grant clawback or compliance investigations.

Mitigation & Contingency

Mitigation: Implement deduplication using the existing multi-chapter membership service as the source of truth for chapter affiliation. Write test fixtures covering all known multi-chapter edge cases and validate outputs against manually prepared reference exports from NHF.

Contingency: If deduplication cannot be made deterministic for complex hierarchies before release, gate the export behind an org-level feature flag and require NHF to validate a preview export against their manual Excel before enabling in production.

medium impact medium prob dependency

Server-side Dart libraries for Excel generation are less mature than equivalents in Node.js or Python. The chosen library may lack support for Bufdir-required formatting features (merged cells, data validation, specific date formats), requiring significant workaround effort or a library switch mid-implementation.

Mitigation & Contingency

Mitigation: Evaluate the top two Dart xlsx libraries (excel, spreadsheet_decoder) against a Bufdir template sample file before committing. Identify all required formatting features and verify library support in a spike.

Contingency: If no Dart library meets requirements, implement the Excel generation as a Supabase Edge Function in TypeScript using the well-supported ExcelJS library, exposing it to the Dart backend via an internal RPC call.

medium impact medium prob integration

The attachment bundler must retrieve documents from Supabase Storage that were uploaded by the document attachments feature. If storage paths, RLS policies, or signed URL expiry have not been standardised across features, the bundler may fail to retrieve attachments at export time.

Mitigation & Contingency

Mitigation: Audit the document attachments feature's storage schema and RLS policies before implementing the bundler. Agree on a stable internal service-account access pattern for cross-feature storage reads.

Contingency: If cross-feature storage access cannot be made reliable, implement the bundler to include only attachments that can be retrieved successfully and produce a manifest listing any attachments that could not be bundled, rather than failing the entire export.