Staff Newsletter — QA Plan
1. Rollout
| # | Scenario | Expected |
|---|---|---|
| 1.1 | Deploy, org has never saved a config | Exactly one daily update at 06:00 UTC with the cancellation and follow-up sections; no other staff mail |
| 1.2 | Deploy day | No org receives the old retention digest — its job is gone, and the stale schedule is removed at boot |
| 1.3 | Owner opens Settings | The Updates tab is there with no flag set |
2. Permissions
| # | Scenario | Expected |
|---|---|---|
| 2.1 | Owner opens the tab | Renders, all 7 rows |
| 2.2 | Admin opens the tab | Renders |
| 2.3 | Coach navigates to /dashboard/settings/notifications | Bounced to their first visible route |
| 2.4 | Coach PATCHes the endpoint directly | 403, not 404 |
| 2.5 | Member of another org GETs this org’s config | 403 |
3. Cadence configuration
| # | Scenario | Expected |
|---|---|---|
| 3.1 | Change one event, save | PATCH body carries exactly that one key |
| 3.2 | Save with nothing changed | No request; save bar hidden |
| 3.3 | Open the retentionCaseAttention select | No “Immediately” option |
| 3.4 | PATCH {"retentionCaseAttention":"immediately"} by hand | 400, message names the event |
| 3.5 | PATCH {"newLead":"hourly"} | 400 from class-validator |
| 3.6 | Set an event to off explicitly, reload | Still off — a default must not switch it back on |
| 3.7 | Save, then reload | Values persist; save bar hidden |
4. Immediate sends
| # | Scenario | Expected |
|---|---|---|
| 4.1 | newLead = immediately, a lead is created | One mail per active owner/admin, subject names the lead |
| 4.2 | newLead = daily, a lead is created | No immediate mail; the lead appears in tomorrow’s digest |
| 4.3 | newMember = immediately, a coach is added | No mail |
| 4.4 | newMember = immediately, a member activates | One mail |
| 4.5 | paymentFailed = immediately, first decline | One mail; amount formatted in each recipient’s locale |
| 4.6 | Same subscription’s 3rd decline with newDebt = immediately | Both a paymentFailed and a newDebt mail |
| 4.7 | 4th decline (balance grows) | paymentFailed only — no second newDebt |
| 4.8 | Debt cleared, then a new debt opens | newDebt fires again (debt_since was reset to null) |
| 4.9 | The listener’s DB read fails | Error logged; the member’s cancellation / charge record is unaffected |
| 4.10 | Member requests cancellation in-app, cancellationRequest = immediately | One mail; the member’s request itself still succeeds |
| 4.11 | Staff record a notice for the same subscription afterwards | No second immediate mail (kind promotes to notice) |
5. Digests
| # | Scenario | Expected |
|---|---|---|
| 5.1 | Daily tick, nothing happened in the window | No mail, and no audit claim — so a later run can still report it |
| 5.2 | Daily tick, two events have rows | One mail with two sections, in catalog order |
| 5.3 | Daily tick runs twice | Second sends nothing |
| 5.4 | Manual trigger races the scheduled tick | One sends; the other logs the held lock |
| 5.5 | Weekly tick on Monday, then re-triggered Wednesday | Second skipped — same ISO week key |
| 5.6 | Weekly tick spanning a new year (e.g. 2027-01-01) | Key is 2026-W53; a Monday and a Sunday of one week share a key |
| 5.7 | One org’s digest query throws | Logged; later orgs still get theirs |
| 5.8 | retentionCaseAttention = daily | Two sub-sections under their own headings — the split the old digest made, in this feature’s own friendlier wording |
| 5.9 | An open case is both past SLA and in its final week | Appears in both sub-sections |
| 5.10 | Org has an active admin but no owner | Still in orgIdsWithStaff(); the admin is mailed |
| 5.11 | Suspended owner | Not mailed |
6. Email rendering
Run pnpm email:preview and read staff-newsletter-* in all three locales.
| # | Scenario | Expected |
|---|---|---|
| 6.1 | Hebrew recipient | RTL throughout; sections and rows read correctly |
| 6.2 | Money row in en vs ru | Both show 240; formatting differs — proof it is per recipient |
| 6.3 | Person name contains <script> | Escaped in the body |
| 6.4 | Person name contains an apostrophe | Literal in the subject, not ' |
| 6.5 | Unresolvable person | Localized fallback, never the English “Unknown” |
| 6.6 | Org has no logo | Letterhead degrades as every other template does |
| 6.7 | Org has no contact_email | No Reply-To, no “just reply” footer line |
| 6.8 | Section with zero rows | Heading omitted too |
7. Regression — the retired retention digest
| # | Scenario | Expected |
|---|---|---|
| 7.1 | Default config | The same three sections the retention digest carried arrive daily, in the newsletter’s structure |
| 7.2 | Org sets cancellationRequest = off | The notices section disappears; the follow-up sections stay |
| 7.3 | Org sets both retention events off | No staff mail about cancellations at all — the escalation audit rows and cancellation_review tasks still happen |