Legal — QA Plan
1. Document fetch
| # | Scenario | Expected |
|---|---|---|
| 1.1 | GET /legal/documents with no locale | Returns latest per type in he |
| 1.2 | GET /legal/documents?locale=en | Returns latest English version of each type |
| 1.3 | Two terms_of_use rows with same published_at | Deterministic tiebreak by id ASC |
| 1.4 | Locale missing for a type (e.g. no ru cookie_policy) | That type omitted from response |
2. Consent recording
| # | Scenario | Expected |
|---|---|---|
| 2.1 | New owner accepts platform Terms during signup | One legal_consents row inserted with context='owner_onboarding', IP + UA captured |
| 2.2 | User accepts the same set again | No new rows — recordConsent filters already-consented document IDs |
| 2.3 | TOS bumped to v2; user re-accepts via reconsent flow | New row for v2 inserted with context='reconsent'; v1 row remains |
| 2.4 | Request behind proxy with X-Forwarded-For: 1.2.3.4 | ip_address='1.2.3.4' |
| 2.5 | Request with no user agent | user_agent NULL in DB |
| 2.6 | Consent recorded with organizationId set | Row carries the org id; external acknowledgement uses it as required scope, other types as audit context |
3. Consent status / gating
| # | Scenario | Expected |
|---|---|---|
| 3.1 | Active member has accepted no documents | Access gated on privacy, fitness acknowledgement and member terms, never owner platform Terms |
| 3.2 | Active owner has not accepted current platform Terms | Gated only on terms_of_use; privacy, health acknowledgement and member terms are absent |
| 3.3 | Active admin or coach has no staff-specific terms | Access allowed; member and owner documents are not presented |
| 3.4 | User accepted every server-required current version in the active organization | Access allowed |
| 3.5 | Concurrent acceptances on two devices | Non-unique ledger index permits duplicate evidence if both pass the existence check; no unique-constraint failure |
4. Web UI
| # | Scenario | Expected |
|---|---|---|
| 4.1 | Acceptance form in Hebrew | RTL layout, links open marketing-site Hebrew page |
| 4.2 | Click marketing link with no internet | Graceful — link target unreachable but form still submittable |
| 4.3 | Check all boxes, click Accept | API call sends correct types array; on success, dialog dismisses |
| 4.4 | Uncheck a required box | Submit button disabled / form invalid |
| 4.5 | Embedded mode (standalone=false) | No API call from the form. Parent receives onAcceptedChange(true/false) on every tick/untick and onAccepted when all boxes are ticked |
5. Permissions
| # | Scenario | Expected |
|---|---|---|
| 5.1 | Unauthenticated GET /legal/documents | 200 — public route |
| 5.2 | Unauthenticated POST /legal/consents | 401 |
| 5.3 | Authenticated user fetches their own status | 200 |
| 5.4 | User cannot fetch another user’s status | No such endpoint exists — status is always self |
6. Edge cases
| # | Scenario | Expected |
|---|---|---|
| 6.1 | recordConsent with empty types array | IsArray + IsEnum each: true rejects — 400 |
| 6.2 | recordConsent with unknown type | 400 from DTO validator |
| 6.3 | recordConsent with malformed UUID organizationId | 400 |
| 6.4 | DB connection drops mid-insert | Caught error; partial rows for some types possible (atomicity is per-statement only — not wrapped in a transaction) |
7. Smoke
- New owner can complete signup with the acceptance step.
- Member onboarding picks up
context='member_onboarding'. - Status endpoint returns
requiresReconsent=trueafter a doc bump. - Banner appears in Hebrew RTL with correct copy.
8. Member legal acceptance (review scenarios; not executed here)
| # | Scenario | Expected |
|---|---|---|
| 8.1 | Existing active member has current privacy + fitness acceptance but no current member terms | Existing /accept-terms requests member terms only; no compliance form, privacy/fitness re-sign or synthetic consent |
| 8.2 | Native payment provider, no plan payment-link URL | Required join array includes privacy, fitness acknowledgement and member_terms; no staff Terms in the member-join contract |
| 8.3 | Server-resolved plan payment-link URL or active external provider | Separate external acknowledgement is required alongside member terms and the base bundle |
| 8.4 | Bypass UI and omit a required boolean on invitation request or authenticated direct accept | Server rejects before membership/invitation mutation; analytics consent never substitutes for legal acceptance |
| 8.5 | Payment configuration changes after public resolve | Submit recomputes payment requirements and rejects incomplete newly required acceptance |
| 8.6 | Accept each new document in he, en, and ru | Evidence references the current exact localized version, with context/IP/UA/time and organization; Hebrew links have no /he prefix |
| 8.7 | External acknowledgement exists for organization A; join organization B | A’s evidence does not satisfy B; same-organization current evidence is deduplicated |
| 8.8 | New required member/external document version is published | Old consent remains evidence, but does not satisfy current required status |
| 8.9 | Switch organization at /users/me and consent-status, including stale explicit selection | Required types and returned organizationId follow valid explicit org then membership role priority; stale/omitted selection falls back; active-member backstop never includes external acknowledgement |
| 8.10 | Open /accept-terms as a member who is staff elsewhere | Requirements follow the selected organization only: member documents appear in the member org and no member documents appear in the staff org |
| 8.11 | Review launch prerequisites | Israeli counsel approves member terms and external-payment acknowledgement before launch; document publication never inserts acceptance on behalf of members |
| 8.12 | Pending-only member joins with active external provider | Distinct external acknowledgement checkbox is required before activation; returned status preserves target organization scope |
| 8.13 | Staff creates or invites a member without required consent | Creation/invitation succeeds without validating or recording member consent; the member accepts through /accept-terms at first login and pending invitations activate only after required consent |
| 8.14 | Read Fitness & Health Acknowledgement | Display title is updated; fitness_waiver type/slug stay unchanged; mandatory statutory rights are not waived |
| 8.15 | Member join requires privacy, fitness acknowledgement and member terms | All three documents are linked under one required legal checkbox; no separate fitness or member-terms checkbox appears |
| 8.16 | Analytics/session replay on first onboarding render | Separate optional checkbox is unchecked; required legal submit can proceed without it |
| 8.17 | Inspect mobile PostHog replay config after opt-in | Text inputs, images and sandboxed views are globally masked; log and network telemetry capture are false |
| 8.18 | Open a plan-less join link while signed out | Identity fields, required legal agreement, optional analytics, and Join submit are one step; no later confirmation screen appears |
| 8.19 | Submit English/Russian join consent with a required catalog row missing | Invitation ticket is not returned and no subset of the requested legal types is recorded |