diplomail (Stage C): paid-tier broadcast + multi-game + cleanup
Tests · Go / test (pull_request) Successful in 1m59s
Tests · Go / test (push) Successful in 1m59s
Tests · Integration / integration (pull_request) Successful in 1m36s

Closes out the producer-side of the diplomail surface. Paid-tier
players can fan out one personal message to the rest of the active
roster (gated on entitlement_snapshots.is_paid). Site admins gain a
multi-game broadcast (POST /admin/mail/broadcast with `selected` /
`all_running` scopes) and the bulk-purge endpoint that wipes
diplomail rows tied to games finished more than N years ago. An
admin listing (GET /admin/mail/messages) rounds out the
observability surface.

EntitlementReader and GameLookup are new narrow deps wired from
`*user.Service` and `*lobby.Service` in cmd/backend/main; the lobby
service grows a one-off `ListFinishedGamesBefore` helper for the
cleanup path (the cache evicts terminal-state games so the cache
walk is not enough). Stage D will swap LangUndetermined for an
actual body-language detector and add the translation cache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-15 19:02:46 +02:00
parent b3f24cc440
commit 362f92e520
14 changed files with 1423 additions and 4 deletions
+6 -3
View File
@@ -16,7 +16,7 @@ purge, and the language-detection / translation cache.
|-------|-------|--------|
| A | Schema, personal single-recipient send / read / delete, unread badge, push event with body-language `und` | shipped |
| B | Owner / admin sends + lifecycle hooks (paused, cancelled, kick); strict soft-access for kicked players | shipped |
| C | Paid-tier personal broadcast + admin multi-game broadcast + bulk purge | planned |
| C | Paid-tier personal broadcast + admin multi-game broadcast + bulk purge + admin observability | shipped |
| D | Body-language detection (whatlanggo) + translation cache + async worker | planned |
## Tables
@@ -40,14 +40,17 @@ Three Postgres tables in the `backend` schema:
| Action | Caller | Pre-conditions |
|--------|--------|----------------|
| Send personal | user | active membership in game; recipient is active member |
| Paid-tier broadcast | paid-tier user | active membership; recipients = every other active member |
| Send admin (single user) | game owner OR site admin | recipient is any-status member of the game |
| Send admin (broadcast) | game owner OR site admin | recipient scope ∈ `active` / `active_and_removed` / `all_members`; sender excluded |
| Multi-game admin broadcast | site admin | scope `selected` (with `game_ids`) or `all_running` |
| Bulk purge | site admin | `older_than_years >= 1`; targets games with terminal status finished more than N years ago |
| Read message | the recipient | row exists in `diplomail_recipients(message_id, user_id)`; non-active members see admin-kind only |
| Mark read | the recipient | row exists; idempotent if already marked |
| Soft delete | the recipient | `read_at IS NOT NULL` (open-then-delete, item 10) |
Stage C will add the paid-tier player broadcast and the bulk-purge
admin endpoint.
Stage D will add body-language detection (whatlanggo) and the
translation cache + async worker.
System mail is produced internally by lobby lifecycle hooks:
`Service.transition()` emits `game.paused` / `game.cancelled` system