fix: notify on expired anonymisation requests via a daily sweep - #415
Merged
Conversation
haksungjang
force-pushed
the
worktree-agent-ae3342dda56b6391e
branch
from
September 6, 2026 16:18
3e161c3 to
dd640b8
Compare
A pending two-person erasure request expires past its seven-day window, but the only code that applied that expiry ran lazily from inside open_request/approve, scoped to one subject. A subject nobody revisited stayed pending forever with nobody told. Add a daily Celery beat that expires every stale pending request regardless of subject and notifies the requester, the approver if any, and every active super-admin. The query/mutate core is shared with the existing lazy per-subject path (services.user_anonymisation_service ._select_and_expire_due) so the two predicates cannot drift apart. The beat owns and commits its own session before enqueuing notifications, so a broker hiccup can only cost a notification, never the expiry. The notification reuses the existing "approval_pending" enum value (never emitted until now) rather than adding a new notification_kind: that enum is mirrored across a schema Literal, a shared JSON fixture and a frontend array with contract tests pinning all of them together, and this fix is scoped to the backend.
haksungjang
force-pushed
the
worktree-agent-ae3342dda56b6391e
branch
from
September 6, 2026 21:06
dd640b8 to
52d04ff
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #383.
services.user_anonymisation_service._expire_stale_foronly expires a stalependinganonymisation request lazily, from insideopen_request/approve, scoped to the one subject that call touches. A subject nobody revisits stayspendingpast its window forever, with nobody told.tasks.anonymisation_expiry_sweep, 04:00 UTC) that sweeps every subject, expires stalependingrows, and notifies the requester, the approver if the row carries one, and every active super-admin._select_and_expire_due) so the expiry predicate cannot drift between them. The lazy per-subject wrapper (_expire_stale_for) is unchanged in behavior.Notification kind
Notification.kindis a closed Postgres enum mirrored by a schemaLiteral, a shared JSON fixture, and a frontend array, with contract tests pinning all four together. Adding a fifth value needs an additive migration and a frontend PR to keep both contract-test suites green. Since this fix is scoped to the backend, the sweep reuses the existingapproval_pendingkind, which has been valid since the first notification migration but has never had a producer — the frontend already ships a label ("Approval pending" / "승인 대기"), icon and tone for it, unexercised until now. The recipient-facing title/body (not thekindcolumn) say "expired" explicitly. See the module docstring intasks/anonymisation_expiry_sweep.pyfor the full reasoning; a follow-up PR can give this its own kind end-to-end (model + schema + fixture + frontend together).Test plan
ruff check apps/backend— cleanmypy apps/backend— clean (947 source files)tests/unit/tasks/test_anonymisation_expiry_sweep.py) — recipient dedup, notification content, enqueue fan-out, task entry point delegation and never-raises behaviortests/integration/test_anonymisation_expiry_sweep.py, real Postgres) — the task actually commits the expiry (re-read from a separate session), notifies requester + super-admins, dedupes a requester who is also a super-admin, never touches anapprovedrequest, leaves a freshpendingrequest alone, is idempotent on immediate rerun, and a regression guard that the lazy per-subject path still uses the shared predicatetest_anonymise_user_command.py/test_user_anonymisation_api.py/test_catalog_contracts.py/test_celery_app.py— all still pass unchangednode tools/em-dash/lint.mjs— clean