Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesOrganizationsOrganizations — QA Plan

Organizations — QA Plan

Pre-requisites

  • Clerk test instance (so createOrganization works).
  • 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

StepActionExpected
1New user signs up, lands on /{lang}/{lang}/onboarding.Wizard renders.
2Enter org name + type. Finish wizard.POST /organizations returns the new org. clerk_organization_id populated. memberships row inserted with role='owner', status='active'.
3Wizard proceeds to legal consents + onboarding-complete.Welcome email sent.
4GET /users/me.Memberships array now contains the new org with role=owner.

G2 — List my orgs

StepActionExpected
1Owner of orgs A and B. GET /organizations.Returns both.
2Member of A only; GET /organizations.Returns A only.

G3 — Update branding

StepActionExpected
1PATCH /:id with {name:"New Name", contactEmail:"hi@x.com", cancellationWindowHours:4}.200. DB row updated.
2PATCH with {contactEmail:""}.contact_email set to NULL.

G4 — Logo upload (happy path)

StepActionExpected
1POST /:id/logo/upload-url with {mimeType:"image/png", fileSize:200_000}.200 {uploadUrl, r2Key, expiresIn:600}. Redis key set.
2PUT file to uploadUrl (R2 direct).200 from R2.
3POST /:id/logo/confirm.200. organizations.logo_url='orgs/{id}/logo'. Redis key gone.
4GET /:id.Returns full presigned read URL in logoUrl.

G5 — Onboarding complete with class type + invites

StepActionExpected
1Owner 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.
2Response.{classTypeCreated:true, invitationsSent:2, emailSent:true}.

Edge cases

E1 — Logo MIME rejection

StepActionExpected
1POST upload-url with mimeType:"image/gif".400 ‘Unsupported file type. Use JPEG, PNG, or WebP.’.
2Size > 2MB.400 ‘File too large (max 2MB)’.

E2 — Logo confirm without prior upload

StepActionExpected
1POST /:id/logo/confirm cold (no Redis blob).400 ‘Upload session expired. Please try again.’.
2After 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

StepActionExpected
1Coach PATCH /:id.403 ‘Only owners and admins can update the organization’.
2Coach POST upload-url.403 ‘Only owners and admins can update the logo’.

E4 — Non-owner onboarding-complete

StepActionExpected
1Admin POSTs /:id/onboarding-complete.403 ‘Only owners can complete onboarding’.

E5 — Cross-org GET

StepActionExpected
1User with no membership in org B: GET /organizations/{B}.404 ‘Organization not found’.

E6 — Onboarding-complete partial failure

StepActionExpected
1POST with 3 emails, one malformed.Loop continues; invitationsSent reflects only the successes; failures logged via logger.warn.
2POST with no program in org.classTypeCreated:false; no error.
3Welcome email send throws (mock).Logged; emailSent:false; method still 200.

E7 — Slug collision (theoretical)

StepActionExpected
1Two 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)

StepActionExpected
1Platform-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

LangStrings to verify
ensettings.organization.*, welcome email subject/body (apps/api/src/notifications/templates/welcome.ts).
heSame keys, Hebrew.
ruSame keys, Russian.

Expected vs actual

  • After create: organizations row present, memberships row with role='owner', status='active', Clerk org reachable via clerk_organization_id.
  • After logo confirm: logo_url starts with orgs/; R2 object exists at that key; Redis key absent.
  • After onboarding-complete: appropriate downstream rows (class_types, invitations); audit/log entries.