diff --git a/PRD-TRACKER.md b/PRD-TRACKER.md index 9fa4e89f..5b0bd3c3 100644 --- a/PRD-TRACKER.md +++ b/PRD-TRACKER.md @@ -25,6 +25,7 @@ Use this file to keep a single prioritized view of all PRDs. | - | 15 | `prd/webhooks` | todo | | - | - | `prd/zapier` | draft | | - | - | `prd/release-process` | draft | +| - | - | `prd/workshops` | draft | ## DONE (Completed PRDs) diff --git a/prd/workshops/README.md b/prd/workshops/README.md index 143795ba..085dae28 100644 --- a/prd/workshops/README.md +++ b/prd/workshops/README.md @@ -28,6 +28,9 @@ Orgs that run trainings and webinars have single-video content that doesn't fit 7. **Workshops and courses never mix**: excluded from the courses catalog, admin course list, course-type filters, the course create modal, and LMS My Learning / progress / certificate lists. (A workshop certificate issued by a passed assessment — decision 9 — is delivered by email and downloadable from the workshop page; it does not enroll the workshop into LMS progress/completion surfaces.) 8. **v1 scope is all four surfaces**: admin, public org-site pages, landing-page section (all 10 themes), LMS entry. 9. **Optional post-session assessment (CE mode)**: an admin can enable exactly one assessment on a workshop — a quiz with a pass percentage (default 80%) — and registrants who pass are automatically issued a certificate. This is the standard certified-training (continuing-education) workflow — register → attend live → take the test → pass → certificate emailed → replay stays up; the grading, pass-gate, and certificate machinery already exist on courses. Off by default; a workshop without an assessment behaves exactly as decisions 1–8 describe. +10. **Calendar invites ship in v1, not later** — reclassified after auditing the codebase: `.ics` generation (`packages/email/src/ics.ts` `buildSessionIcs`) and attachment-on-enrollment (`services/course/session-invite.ts` `getWelcomeSessionIcs`) already work in production for `LIVE_CLASS` courses, gated by one line (`course.type !== 'LIVE_CLASS'`). Widening that gate to include `WORKSHOP` gives every scheduled-workshop registrant an auto-attached calendar invite (title, time, join link, default reminders) with no new subsystem — see Data Model and Phase 1. +11. **Internal-audience workshops need no new visibility flag — with one exception.** "Published, but only reachable/usable by people already in the organization" is achieved by combining the existing per-workshop gating choice (decision 4) with the org-level `organization.disableSignup` / `settings.signup.inviteOnly` toggles (`apps/api/src/middlewares/signup-guard.ts`, `services/organization/auto-join.ts`): a **registration-required** workshop on an org with signup disabled cannot be joined by a stranger (they can't create the account the enroll flow requires), while existing team members log in and register as usual. **The gap**: the **public (no-login)** gating choice bypasses `disableSignup` entirely — anyone with the link watches regardless of the org's signup settings. The admin UI must warn (or block) choosing "public" gating on an org that has signup disabled, since it silently defeats the org's own intent. +12. **Zero-friction internal delivery is a real, separate feature — deferred.** Even with the combination above, an existing employee still clicks "Register" once (having an org account ≠ having a `groupmember` row on that specific workshop). True zero-click delivery — the workshop just appears already-registered for every teammate — needs a new `metadata.workshop.audience: 'public' | 'organization'` setting that auto-enrolls every current `organizationmember` (role ≥ STUDENT) into the workshop's group on publish, and auto-enrolls new hires as they join (mirroring the existing `autoJoinOrg` pattern). Out of v1; tracked as a Later item, not built alongside decisions 1–9. ## Current-State Audit @@ -67,7 +70,8 @@ Orgs that run trainings and webinars have single-video content that doesn't fit - No-account (email-only) registration — Luma-grade friction reduction ships later (verify-email token → email-only groupmember, idempotent confirm/cancel, rate limits, reconcile to profile). - Paid workshops (course pricing plumbing exists; deferred). - Capacity limits, waitlists, approval, custom registration questions. -- ICS calendar invites + 24h/1h reminders — near-free later (widen the `LIVE_CLASS` gate in `listUpcomingSessionsForReminderScan`, reuse `session-invite.ts`), but out of v1. +- Rescheduling/cancellation calendar updates — `buildSessionIcs` supports `method: 'CANCEL'` but no caller wires it today; a rescheduled or cancelled workshop does not yet push an updated/cancelled calendar event to prior registrants. +- Zero-click internal-audience auto-enrollment (`metadata.workshop.audience` — see Confirmed Decision 12). - Multiple hosts per workshop; replay-available notifications; workshop analytics dashboards; dedicated topic taxonomy (tags suffice); landing-page section reordering (no section-ordering system exists — the section renders after courses). - Assessment extras beyond one quiz + pass mark + certificate: multiple assessments, retake limits, timed tests, manual grading, attendance verification before the quiz unlocks, accreditation-body reporting. @@ -78,6 +82,8 @@ Orgs that run trainings and webinars have single-video content that doesn't fit - Workshop creation (one transaction): group + course (`type: 'WORKSHOP'`) + creator tutor member + the single lesson (title = course title, `public` per gating choice, explicit `lessonAt`, `teacherId` = host, slug via the slug service). No welcome newsfeed. - **Invariants enforced in services, not just UI**: lesson/section create services reject additions to WORKSHOP courses. Exercise creation is likewise rejected, with one controlled exception: the workshop assessment service may create **exactly one** exercise on the single lesson when `assessment.enabled` is turned on (and general exercise routes still reject workshop courses — the assessment service is the only entry point). Publish readiness (extend `go-live-readiness.ts`): title + host required; upcoming needs `lessonAt`; on-demand needs a playable video. - **Security**: anonymous payloads never contain `callUrl`; an authed membership check returns it for enrolled users (and drives the "Registered ✓ / Join" card state). +- **Calendar invite reuse**: widen `getWelcomeSessionIcs`'s `course.type !== 'LIVE_CLASS'` gate (`services/course/session-invite.ts`) to admit `WORKSHOP`, so `buildSessionIcs` attaches a `.ics` to the confirmation email exactly as it does for live classes today (see Confirmed Decision 10). No new email template, no new package. +- **Admin footgun guard**: the workshop editor's gating toggle warns (or blocks) selecting "public, no login" when the org has `disableSignup` enabled, since that combination silently exposes the workshop to anyone despite the org's closed-signup intent (see Confirmed Decision 11). ## API @@ -121,7 +127,8 @@ Reuses `(org-site)/course/[slug]/enroll`, with these deliberate deltas: - **Redirect chain**: the existing flow hard-redirects post-enroll to the LMS (`/courses/{id}/lessons?next=true`) and rebuilds login/signup redirects to the enroll page. Workshops thread a return destination through all hops so the registrant lands back on `/workshops/[slug]` (extend `getStudentCourseContinuePath` or pass an explicit redirect — don't fork the enroll page). - **Email verification gate**: kept — protects `callUrl` and keeps registrant emails real. - **Free reuse**: `allowNewStudent === false` doubles as "registration closed"; `?invite_token=` gives private/unlisted workshops. -- **Confirmation email**: the org-branded student-course-welcome email; verify wording fits workshops (date + call link for scheduled ones). +- **Confirmation email**: the org-branded student-course-welcome email, now with a `.ics` calendar invite attached for scheduled workshops (widened `getWelcomeSessionIcs` gate — Confirmed Decision 10); verify wording fits workshops (date + call link for scheduled ones). +- **Internal-only orgs**: registration-required gating + the org's own `disableSignup`/`inviteOnly` settings already keep strangers out (Confirmed Decision 11) — no workshop-specific code needed here beyond the gating-toggle warning noted in Data Model. ### Landing page (all 10 themes) @@ -145,10 +152,12 @@ Flow mirrors the courses section exactly: 3. Should the enrollment confirmation email get a workshop-specific template variant, or is adjusted generic wording enough? 4. **Assessment certificate template**: reuse the existing course certificate templates as-is, or does a workshop certificate need its own fields (session date, host, duration/credit hours)? CE buyers may need credit hours printed — verify against a real CE customer's certificate before building. 5. Does Phase 5 ship inside v1 (if certified trainings are the go-to-market wedge) or immediately after the four v1 surfaces? Default until decided: build Phases 1–4 first, but design Phase 1 metadata/invariants so the assessment slot needs no migration. +6. Should the "public gating on a signup-disabled org" case (Confirmed Decision 11) hard-block save, or just show a warning the admin can dismiss? Recommendation: warn, don't block — an org might legitimately want one public workshop as a lead magnet while keeping account signup closed. +7. Is zero-click internal auto-enrollment (Confirmed Decision 12 / `metadata.workshop.audience`) worth pulling into v1, or does the registration-required + disableSignup combination cover internal-training customers well enough for launch? ## Phased Delivery -**Phase 1 — Data model & backend**: enum value; `metadata.workshop` settings; `createWorkshop` service; invariant guards + publish readiness; widen the PUBLIC anonymous pipeline; exclusions everywhere (courses catalog default, admin list, LMS student queries, filter UIs, create modal); `/workshop` + org-site routers; team `avatarUrl`; email copy check. +**Phase 1 — Data model & backend**: enum value; `metadata.workshop` settings; `createWorkshop` service; invariant guards + publish readiness; widen the PUBLIC anonymous pipeline; widen the `LIVE_CLASS`-only ICS/calendar-invite gate (`session-invite.ts`) to include WORKSHOP; exclusions everywhere (courses catalog default, admin list, LMS student queries, filter UIs, create modal); `/workshop` + org-site routers; team `avatarUrl`; email copy check. **Phase 2 — Admin surface**: sidebar entry; list + single-page editor reusing lesson video/note components; host picker; schedule; gating; translations (`en.json` + `pnpm translate`). @@ -158,7 +167,7 @@ Flow mirrors the courses section exactly: **Phase 5 — Assessment & certificates (CE mode)**: `metadata.workshop.assessment`; assessment service (sole entry point that may create the single exercise); editor assessment section + results view; workshop-page assessment flow (quiz → auto-grade → pass gate → certificate email + download); certificate stays out of LMS progress/completion lists. -**Later**: no-account registration, ICS + reminders (near-free via existing session machinery), paid workshops, capacity/waitlists, replay notifications, workshop analytics, multiple hosts, featured collections, assessment extras (retakes, timing, attendance gating, accreditation reporting). +**Later**: no-account registration, reschedule/cancel calendar updates (`method: 'CANCEL'` wiring), `metadata.workshop.audience` zero-click internal auto-enrollment, paid workshops, capacity/waitlists, replay notifications, workshop analytics, multiple hosts, featured collections, assessment extras (retakes, timing, attendance gating, accreditation reporting). ## Verification @@ -168,5 +177,6 @@ Flow mirrors the courses section exactly: 1. Admin: create → video + note + team-member host → publish. Not in Courses list; not in the course create modal. 2. Incognito: landing section (after enabling) → `/workshops` → card (host avatar + duration) → detail plays video, callout renders. 3. Gating: registration-required → Register → sign up → returned to the workshop page → video plays; student absent from LMS My Learning, present under `/lms/workshops`. - 4. Upcoming: future `lessonAt` + `callUrl` → "Upcoming events" card with date; register → confirmation email; call link visible only to enrolled; anonymous API response contains no `callUrl`. Add recording after the date → same URL now on-demand. - 5. Assessment (Phase 5): enable assessment + 80% pass mark → registered student takes quiz on the workshop page → scoring above threshold emails a certificate and shows the download; scoring below shows the score with no certificate; a second exercise cannot be created through any route; the workshop still absent from LMS My Learning and completion/certificate lists. + 4. Upcoming: future `lessonAt` + `callUrl` → "Upcoming events" card with date; register → confirmation email with a `.ics` attachment (title, time, join link, reminders) that the mail client offers to add to calendar; call link visible only to enrolled; anonymous API response contains no `callUrl`. Add recording after the date → same URL now on-demand. + 5. Internal-only: set org `disableSignup: true`, workshop gating "registration-required" → an unauthenticated stranger hitting `/workshops/[slug]` is redirected to signup and blocked; an existing team member logs in and registers normally. Re-run with gating set to "public" → confirm the admin UI surfaces the disableSignup-conflict warning. + 6. Assessment (Phase 5): enable assessment + 80% pass mark → registered student takes quiz on the workshop page → scoring above threshold emails a certificate and shows the download; scoring below shows the score with no certificate; a second exercise cannot be created through any route; the workshop still absent from LMS My Learning and completion/certificate lists.