Organizations — QA Plan
Pre-requisites
- Clerk test instance (so
createOrganizationworks). - R2 bucket configured with presigned PUT support.
- Redis reachable (logo upload session).
- Personas: a brand-new signed-in user with no memberships (owner-to-be), plus existing owner/admin/coach/member personas in a seeded org.
Golden paths
G1 — First-time owner creates an org
| Step | Action | Expected |
|---|---|---|
| 1 | New user signs up, lands on /{lang} → /{lang}/onboarding. | Wizard renders. |
| 2 | Enter org name + type. Finish wizard. | POST /organizations returns the new org. clerk_organization_id populated. memberships row inserted with role='owner', status='active'. |
| 3 | Wizard proceeds to legal consents + onboarding-complete. | Welcome email sent. |
| 4 | GET /users/me. | Memberships array now contains the new org with role=owner. |
G2 — List my orgs
| Step | Action | Expected |
|---|---|---|
| 1 | Owner of orgs A and B. GET /organizations. | Returns both. |
| 2 | Member of A only; GET /organizations. | Returns A only. |
G3 — Update branding
| Step | Action | Expected |
|---|---|---|
| 1 | PATCH /:id with {name:"New Name", contactEmail:"hi@x.com", cancellationWindowHours:4}. | 200. DB row updated. |
| 2 | PATCH with {contactEmail:""}. | contact_email set to NULL. |
G4 — Logo upload (happy path)
| Step | Action | Expected |
|---|---|---|
| 1 | POST /:id/logo/upload-url with {mimeType:"image/png", fileSize:200_000}. | 200 {uploadUrl, r2Key, expiresIn:600}. Redis key set. |
| 2 | PUT file to uploadUrl (R2 direct). | 200 from R2. |
| 3 | POST /:id/logo/confirm. | 200. organizations.logo_url='orgs/{id}/logo'. Redis key gone. |
| 4 | GET /:id. | Returns full presigned read URL in logoUrl. |
G5 — Onboarding complete with class type + invites
| Step | Action | Expected |
|---|---|---|
| 1 | Owner with at least one program in the org. POST /:id/onboarding-complete with {classTypeName:"WOD", defaultDurationMin:60, defaultCapacity:20, inviteEmails:["a@x.com","b@x.com"]}. | Class type inserted under the org’s first program. Both invitations created (Clerk + DB). Welcome email sent. |
| 2 | Response. | {classTypeCreated:true, invitationsSent:2, emailSent:true}. |
Edge cases
E1 — Logo MIME rejection
| Step | Action | Expected |
|---|---|---|
| 1 | POST upload-url with mimeType:"image/gif". | 400 ‘Unsupported file type. Use JPEG, PNG, or WebP.’. |
| 2 | Size > 2MB. | 400 ‘File too large (max 2MB)’. |
E2 — Logo confirm without prior upload
| Step | Action | Expected |
|---|---|---|
| 1 | POST /:id/logo/confirm cold (no Redis blob). | 400 ‘Upload session expired. Please try again.’. |
| 2 | After upload-url but without PUT’ing the file. | R2 objectExists returns false → 400 ‘Upload not found. Please re-upload the file.’. |
E3 — Non-staff updates
| Step | Action | Expected |
|---|---|---|
| 1 | Coach PATCH /:id. | 403 ‘Only owners and admins can update the organization’. |
| 2 | Coach POST upload-url. | 403 ‘Only owners and admins can update the logo’. |
E4 — Non-owner onboarding-complete
| Step | Action | Expected |
|---|---|---|
| 1 | Admin POSTs /:id/onboarding-complete. | 403 ‘Only owners can complete onboarding’. |
E5 — Cross-org GET
| Step | Action | Expected |
|---|---|---|
| 1 | User with no membership in org B: GET /organizations/{B}. | 404 ‘Organization not found’. |
E6 — Onboarding-complete partial failure
| Step | Action | Expected |
|---|---|---|
| 1 | POST with 3 emails, one malformed. | Loop continues; invitationsSent reflects only the successes; failures logged via logger.warn. |
| 2 | POST with no program in org. | classTypeCreated:false; no error. |
| 3 | Welcome email send throws (mock). | Logged; emailSent:false; method still 200. |
E7 — Slug collision (theoretical)
| Step | Action | Expected |
|---|---|---|
| 1 | Two orgs with identical names created concurrently — both hash to the same slug suffix (vanishingly small probability). | UNIQUE violation on second insert. Currently unhandled (would 500). TODO: verify whether retry layer exists. |
E8 — Tier changes (out of band)
| Step | Action | Expected |
|---|---|---|
| 1 | Platform-billing upgrades org to pro. platform_tier='pro', platform_tier_updated_at=now. | Tier-gated endpoints (@RequiresFeature('lead_management'), location create, etc.) reflect the new tier immediately. |
Cross-persona
- Owner: full control.
- Admin: same as owner except
onboarding-complete. - Coach: read-only on org settings; cannot upload logo or update.
- Member: read-only; only sees orgs where they have an active membership.
i18n
| Lang | Strings to verify |
|---|---|
| en | settings.organization.*, welcome email subject/body (apps/api/src/notifications/templates/welcome.ts). |
| he | Same keys, Hebrew. |
| ru | Same keys, Russian. |
Expected vs actual
- After create:
organizationsrow present,membershipsrow withrole='owner', status='active', Clerk org reachable viaclerk_organization_id. - After logo confirm:
logo_urlstarts withorgs/; R2 object exists at that key; Redis key absent. - After onboarding-complete: appropriate downstream rows (class_types, invitations); audit/log entries.