namespace BigRegister.Api.Data; /// /// Stored document metadata. The demo deliberately keeps NO file content — only /// metadata — which is enough for status/delete/link and avoids holding PII bytes /// in memory. A real backend persists the bytes to blob storage keyed by DocumentId. /// public sealed record StoredDocument( string DocumentId, string LocalId, string CategoryId, string WizardId, string FileName, long SizeBytes, string Owner, DateTimeOffset UploadedAt) { public bool Linked { get; set; } } public sealed record AuditEntry(DateTimeOffset At, string Action, string DocumentId, string CategoryId, string Actor); /// /// In-memory document store + audit log (no DB). ponytail: one global lock — fine /// for a single-process demo store; swap for per-key locks if it ever serves load. /// The audit log holds metadata only (never file content or other PII). /// public static class DocumentStore { /// The single seeded user (the demo has no real auth; ownership = this id). public const string DemoOwner = "19012345601"; private static readonly Dictionary _docs = new(); private static readonly List _audit = new(); private static readonly object _gate = new(); public static StoredDocument Add(string localId, string categoryId, string wizardId, string fileName, long sizeBytes, string owner) { var doc = new StoredDocument(Guid.NewGuid().ToString(), localId, categoryId, wizardId, fileName, sizeBytes, owner, DateTimeOffset.UtcNow); lock (_gate) _docs[doc.DocumentId] = doc; Audit("upload", doc.DocumentId, categoryId, owner); return doc; } public static StoredDocument? Get(string documentId) { lock (_gate) return _docs.TryGetValue(documentId, out var d) ? d : null; } /// Status for the poll-on-return pattern: a known localId is "complete" (it /// arrived), an unknown one is still in flight / never started. public static IReadOnlyList ByLocalIds(IEnumerable localIds) { var set = localIds.ToHashSet(); lock (_gate) return _docs.Values.Where(d => set.Contains(d.LocalId)).ToList(); } /// Mark digital documents as linked to a finalised submission (blocks user delete). public static void Link(IEnumerable documentIds) { lock (_gate) foreach (var id in documentIds) if (_docs.TryGetValue(id, out var d)) d.Linked = true; } public enum DeleteResult { Ok, NotFound, Linked } /// User delete: owner-scoped; blocked once linked to a finalised submission. public static DeleteResult DeleteOwned(string documentId, string owner) { string categoryId; lock (_gate) { if (!_docs.TryGetValue(documentId, out var d) || d.Owner != owner) return DeleteResult.NotFound; if (d.Linked) return DeleteResult.Linked; categoryId = d.CategoryId; _docs.Remove(documentId); } Audit("delete-user", documentId, categoryId, owner); return DeleteResult.Ok; } /// Admin delete: bypasses ownership, unlinks, and (seam) flags the submission for /// review so a caseworker is notified. Returns false if the document is unknown. public static bool AdminDelete(string documentId, string actor) { string categoryId; lock (_gate) { if (!_docs.TryGetValue(documentId, out var d)) return false; categoryId = d.CategoryId; _docs.Remove(documentId); } Audit("delete-admin", documentId, categoryId, actor); return true; } public static void Audit(string action, string documentId, string categoryId, string actor) { lock (_gate) _audit.Add(new AuditEntry(DateTimeOffset.UtcNow, action, documentId, categoryId, actor)); } public static IReadOnlyList AuditLog { get { lock (_gate) return _audit.ToList(); } } }