docs(prd): add workshop assessment and certificate (CE mode) to workshops PRD

This commit is contained in:
Rotimi Best
2026-07-20 00:59:53 +01:00
parent 8e5d6e4bd9
commit 16803bfa3d
+14 -4
View File
@@ -25,8 +25,9 @@ Orgs that run trainings and webinars have single-video content that doesn't fit
4. **Watch gating is the admin's choice per workshop**: public (anyone watches, no login) or registration-required. Maps to the lesson `public` flag. 4. **Watch gating is the admin's choice per workshop**: public (anyone watches, no login) or registration-required. Maps to the lesson `public` flag.
5. **Registration = enrollment** (existing enroll flow). Registered users see the meeting link and get a confirmation email. Optional per-workshop external registration URL (Luma escape hatch) is a cheap, cuttable add. 5. **Registration = enrollment** (existing enroll flow). Registered users see the meeting link and get a confirmation email. Optional per-workshop external registration URL (Luma escape hatch) is a cheap, cuttable add.
6. **Topics = existing course tags.** On-demand lists group by tag; untagged fall into "General". 6. **Topics = existing course tags.** On-demand lists group by tag; untagged fall into "General".
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 / certificates. 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. 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.
## Current-State Audit ## Current-State Audit
@@ -59,6 +60,7 @@ Orgs that run trainings and webinars have single-video content that doesn't fit
6. Orgs can showcase workshops on their landing page (all 10 themes) with a section toggle, heading, and item limit. 6. Orgs can showcase workshops on their landing page (all 10 themes) with a section toggle, heading, and item limit.
7. Learners see their registered/available workshops in the LMS, linking to the org-site workshop page. 7. Learners see their registered/available workshops in the LMS, linking to the org-site workshop page.
8. Courses and workshops remain fully separated in every list, filter, count, and progress surface. 8. Courses and workshops remain fully separated in every list, filter, count, and progress surface.
9. An admin running certified trainings (CE) can enable a post-session assessment: registrants take the quiz on the workshop page after the session, it is auto-graded, and everyone at or above the pass percentage is automatically issued and emailed a certificate — no manual steps between registration and certificate delivery.
## Non-Goals (v1) ## Non-Goals (v1)
@@ -67,13 +69,14 @@ Orgs that run trainings and webinars have single-video content that doesn't fit
- Capacity limits, waitlists, approval, custom registration questions. - 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. - ICS calendar invites + 24h/1h reminders — near-free later (widen the `LIVE_CLASS` gate in `listUpcomingSessionsForReminderScan`, reuse `session-invite.ts`), but out of v1.
- 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). - 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.
## Data Model ## Data Model
- Add `'WORKSHOP'` to `COURSE_TYPE_VALUES` (`packages/utils/src/constants/course-type.ts`); the pgEnum, Drizzle types, and Zod mirror follow from that one edit. Schema work stops at a passing `@cio/db` build (migrations handled outside this workflow). - Add `'WORKSHOP'` to `COURSE_TYPE_VALUES` (`packages/utils/src/constants/course-type.ts`); the pgEnum, Drizzle types, and Zod mirror follow from that one edit. Schema work stops at a passing `@cio/db` build (migrations handled outside this workflow).
- Course `metadata` gains scalar settings only: `workshop?: { durationMinutes?: number; registrationMode?: 'enrollment' | 'external'; registrationUrl?: string }`. - Course `metadata` gains scalar settings only: `workshop?: { durationMinutes?: number; registrationMode?: 'enrollment' | 'external'; registrationUrl?: string; assessment?: { enabled: boolean; passPercentage: number } }`.
- 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. - 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/exercise create services reject additions to WORKSHOP courses. Publish readiness (extend `go-live-readiness.ts`): title + host required; upcoming needs `lessonAt`; on-demand needs a playable video. - **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). - **Security**: anonymous payloads never contain `callUrl`; an authed membership check returns it for enrolled users (and drives the "Registered ✓ / Join" card state).
## API ## API
@@ -101,12 +104,14 @@ No `DELETE /workshop` (course delete works). Default course queries (`getPublish
- New "Workshops" entry in the org sidebar (`content` group, beside Courses). - New "Workshops" entry in the org sidebar (`content` group, beside Courses).
- List page: cards with title, host, derived-state badge, published status. - List page: cards with title, host, derived-state badge, published status.
- Single-page editor (`/workshops/[id]`) — no course sidebar: title, description, host picker (team members from `GET /organization/team`, which gains `avatarUrl`), schedule (`lessonAt` + `durationMinutes`), video + rich-text note (reusing `features/course/components/lesson/` `video/*` and `note/*` by hydrating the existing lesson store), callout, tags, gating toggle, registration mode, publish. - Single-page editor (`/workshops/[id]`) — no course sidebar: title, description, host picker (team members from `GET /organization/team`, which gains `avatarUrl`), schedule (`lessonAt` + `durationMinutes`), video + rich-text note (reusing `features/course/components/lesson/` `video/*` and `note/*` by hydrating the existing lesson store), callout, tags, gating toggle, registration mode, publish.
- Assessment section (Phase 5): enable toggle, pass percentage (default 80), and the quiz builder (reusing the existing exercise question editor against the single lesson's one exercise). Enabling requires registration-required gating — anonymous viewers can't take a graded, certificate-issuing test. Results view lists registrants with score, pass/fail, and certificate status.
- `/courses/[id]` for a workshop-typed course redirects to `/workshops/[id]`. - `/courses/[id]` for a workshop-typed course redirects to `/workshops/[id]`.
### Public org site ### Public org site
- `/workshops` — full catalog: **Upcoming events** (date, Register; "Join" while live) then **On-demand sessions grouped by tag** (duration, Watch; "Replay coming soon" for replay-pending). Theme tokens via the same pattern as `/courses`. - `/workshops` — full catalog: **Upcoming events** (date, Register; "Join" while live) then **On-demand sessions grouped by tag** (duration, Watch; "Replay coming soon" for replay-pending). Theme tokens via the same pattern as `/courses`.
- `/workshops/[slug]` — stable URL through every state: host header (`course-instructor.svelte` pattern) + `lesson-view.svelte` (video, note, callout). Public → plays for anyone; gated → Register; upcoming → date + Register, meeting link for registered users. Admin adds the recording after the event and the same page becomes on-demand automatically. - `/workshops/[slug]` — stable URL through every state: host header (`course-instructor.svelte` pattern) + `lesson-view.svelte` (video, note, callout). Public → plays for anyone; gated → Register; upcoming → date + Register, meeting link for registered users. Admin adds the recording after the event and the same page becomes on-demand automatically.
- With an assessment enabled, registered users see a "Take the assessment" section on the same page once the session is past (or on-demand video watched for evergreen enrollees): quiz → auto-grade → pass ≥ threshold → certificate emailed + downloadable there; below threshold → score shown with the org's contact path. Anonymous visitors see that a certificate assessment exists (conversion hook) but must register.
- Org-site nav shows a Workshops link when the org has published workshops. - Org-site nav shows a Workshops link when the org has published workshops.
### Enrollment (registration) flow — traced ### Enrollment (registration) flow — traced
@@ -138,6 +143,8 @@ Flow mirrors the courses section exactly:
1. **Do workshop registrants count toward the org's plan student limit?** Enrollment creates a student `groupmember`, and `studentLimitReached` blocks signups — a popular free webinar collides with per-seat limits. v1 default until decided: they count; surface the limit clearly in admin. Alternative: exclude WORKSHOP groups from `countActiveStudents` (needs an abuse guard). 1. **Do workshop registrants count toward the org's plan student limit?** Enrollment creates a student `groupmember`, and `studentLimitReached` blocks signups — a popular free webinar collides with per-seat limits. v1 default until decided: they count; surface the limit clearly in admin. Alternative: exclude WORKSHOP groups from `countActiveStudents` (needs an abuse guard).
2. Keep the external `registrationUrl` escape hatch in v1, or cut for leanness? 2. Keep the external `registrationUrl` escape hatch in v1, or cut for leanness?
3. Should the enrollment confirmation email get a workshop-specific template variant, or is adjusted generic wording enough? 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.
## Phased Delivery ## Phased Delivery
@@ -149,7 +156,9 @@ Flow mirrors the courses section exactly:
**Phase 4 — LMS + landing**: `/lms/workshops`; shared landing section + card with per-theme override hook; `OrgLandingPageJson.workshops` config; editor panel; 10 theme `org.svelte` conditional blocks; storybook fixtures; `ui:` prefix checks. **Phase 4 — LMS + landing**: `/lms/workshops`; shared landing section + card with per-theme override hook; `OrgLandingPageJson.workshops` config; editor panel; 10 theme `org.svelte` conditional blocks; storybook fixtures; `ui:` prefix checks.
**Later**: no-account registration, ICS + reminders (near-free via existing session machinery), paid workshops, capacity/waitlists, replay notifications, workshop analytics, multiple hosts, featured collections. **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).
## Verification ## Verification
@@ -160,3 +169,4 @@ Flow mirrors the courses section exactly:
2. Incognito: landing section (after enabling) → `/workshops` → card (host avatar + duration) → detail plays video, callout renders. 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`. 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. 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.