Workflow: Tutor Onboarding
Technical reference for the tutor onboarding workflow module. This page documents the canonical persisted states, the normalized projection returned to consumers, the command surface, the routes that use it, and the precise downstream effects on tutor access.
Canonical persisted states tutor_profiles.onboarding_status is normalized to lowercase snake_case only: invite_pending, intake_in_progress, pending_review, approved, rejected. Legacy values such as Pending_Review, completed, and transitioned_pending are no longer valid persisted states.
Module Contract
| Concern | Implementation | What it does |
|---|
| Workflow module | workers_nivaa/src/lib/workflows/tutor-onboarding/ | Owns the tutor onboarding aggregate, normalized projection, and reviewability logic. |
| Aggregate | types.ts + repository.ts | Loads tutor users row, tutor_profiles row, invite-placeholder state, and capability booleans. |
| Projection | guards.ts + projections.ts | Maps raw persisted state into a normalized UI/API shape such as approved, pending_review, or rejected plus next_step and access booleans. |
| Commands | commands.ts | Defines observe_tutor_onboarding, decide_tutor_onboarding, and the status resolution helper used after intake submission. |
| Effects | effects.ts | Packages workflow results in the shared workflow result shape so routes can emit consistent metadata. |
| Shared contract | workers_nivaa/src/lib/workflows/core/contracts.ts | Provides actor/context/result/event/notification shapes shared across workflow modules. |
Technical Inputs And Outputs
| Surface | Reads | Writes / Decisions | Impacts |
|---|
GET /intake/tutor | Tutor aggregate + projection | None | Drives tutor workspace gate, profile shell, and onboarding CTA state. |
POST /intake/tutor | Existing raw status + profile completeness | Route writes tutor profile fields and then uses workflow helper to resolve the next persisted status | Moves rejected or incomplete tutors back into pending_review when appropriate, then emits coordinator-facing review notifications. |
PUT /admin/tutors/:id/onboarding | Reviewable raw status from workflow | Admin route persists approved or rejected after workflow validation | Unlocks or blocks tutor workspace/postings access and removes tutors from the review queue. |
| Tutor workspace layout | Projection status + capability booleans | None | Shows pending/rejected/approved gate screens without re-implementing state mapping. |
| Tutor postings access | Projection can_browse_postings | None | Blocks non-approved tutors from /postings/browse and application flows. |
| Telegram tutor commands | Projection + access guards | None | Prevents Telegram from becoming a side-channel bypass for unfinished onboarding. |
GET /intake/tutor Contract
Baseline particulars contract GET /intake/tutor is not only an onboarding-status endpoint. It is the canonical tutor-profile read contract for /tutor/profile. The response must always provide a usable baseline for tutor particulars even when tutor_profiles is sparse or missing.
type TutorIntakeReadResponse = {
nric_name: string | null;
display_name: string | null;
contact_number: string | null;
address: string | null;
gender?: string | null;
dob?: string | null;
race?: string | null;
employment_type?: string | null;
postal_code?: string | null;
area?: string | null;
languages: string[];
special_needs_experience: string[];
subjects: Array<string | { subject: string; level?: string; stage?: string }>;
grades?: number[];
schedule: unknown[];
onboarding_status?: string | null;
onboarding: TutorOnboardingProjection;
};
| Field | Primary source | Fallback | Reason |
|---|
nric_name | tutor_profiles.nric_name | users.display_name | The profile page labels this as Full Name; tutors created through the normal onboarding flow may only have users.display_name populated. |
display_name | users.display_name | null | Provides the canonical user-level display name separately from the historical nric_name field. |
contact_number | tutor_profiles.contact_number | users.phone | Legacy or invite-only tutor rows may not have copied profile contact details yet. |
address | tutor_profiles.address | users.address_data.full_address | Legacy/imported tutors may have the user-level address but no profile-level address. |
languages, subjects, schedule, special_needs_experience | tutor_profiles / hard_constraints | Empty arrays when no tutor profile row exists | The page should render a usable empty state, not undefined fields. |
Detailed Lifecycle
| Step | Trigger | Persisted state | Workflow interpretation | What it impacts next |
|---|
| 1. Account exists | Tutor self-registers or coordinator creates the account/invite | invite_pending or intake_in_progress depending on setup state | The workflow sees whether the tutor still has a placeholder invite password and whether intake is materially complete. | Tutor can log in but will be held in the onboarding gate. |
| 2. Invite completion | POST /auth/complete-invite or equivalent login/setup path | Usually remains invite_pending until intake is complete | Projection can move from invite_pending toward intake_in_progress once the placeholder-password gate is cleared. | Tutor can access /tutor/profile and continue setup. |
| 3. Intake submission | POST /intake/tutor | Resolved via resolveTutorOnboardingStatusAfterIntake() | If the profile is ready for review, the helper returns pending_review. The route then emits coordinator in-app notifications and secondary Telegram fanout. If the profile is not ready, it preserves the current raw status. | Coordinator tutor review queue and tutor gate state update immediately. |
| 4. Coordinator review | PUT /admin/tutors/:id/onboarding | approved or rejected | decideTutorOnboardingCommand validates that the current raw status is reviewable, persists the decision, and returns the updated aggregate for response projection. | Approved tutors get workspace + postings access; rejected tutors get update-and-resubmit UX. |
| 5. Rejected resubmission | Tutor edits profile and resubmits intake | pending_review | The workflow helper clears the rejection note and returns the tutor to review rather than leaving them stranded in rejected. | Coordinator sees the tutor back in review; tutor sees a pending state instead of a rejection state. |
| 6. Approved operational access | Tutor opens workspace, postings, or Telegram | approved | Projection sets can_access_workspace = true and can_browse_postings = true. | Tutor can browse postings, apply, and use normal tutor workspace surfaces. |
Projection Shape
type TutorOnboardingProjection = {
status: 'account_created' | 'invite_pending' | 'intake_in_progress' | 'pending_review' | 'approved' | 'rejected';
raw_status: string | null;
can_complete_invite: boolean;
can_submit_for_review: boolean;
can_access_workspace: boolean;
can_browse_postings: boolean;
rejection_reason: string | null;
next_step: string;
};
Tables And Fields Touched
users: id, email, display_name, phone, created_at plus placeholder-password detection for invite state.tutor_profiles: onboarding_status, rejection_reason, contact/address/introduction/intake fields, launch_cohort for rollout tagging.- Read-only capability booleans are derived in the aggregate and must not be persisted separately.