Skip to content

Excusal and overflow volunteer workflow

Verified against the workspace on 15 September 2026. Local service implementation, route controllers, validation schemas, and unit test suites are verified clean; complete authenticated browser acceptance across deployed services remains outstanding.

Who does what?

flowchart TD
  A[Tutor needs to miss a session] --> B[Open Excusals page]
  B --> C[Select active allocation, session date, and reason]
  C --> D[Submit excusal request]
  D --> E{Organiser reviews excusal}
  E -->|Decline| F[Excusal marked DECLINED with reason; tutor notified]
  E -->|Approve| G[Excusal marked APPROVED in transaction]
  G --> H[Auto-create OverflowPost for excused session]
  G --> I[Tutor notified of approval via email and in-app]
  H --> J[Open overflow post broadcast to students via email/notification]
  K[Organiser creates manual overflow post] --> J
  J --> L[Student or tutor views open posts on Volunteers page]
  L --> M{Weekly hours budget sufficient?}
  M -->|No| N[Claim rejected with 422 hours exceeded]
  M -->|Yes| O[Submit claim; creates PENDING OverflowClaim]
  O --> P{Organiser reviews claim}
  P -->|Reject| Q[Claim marked REJECTED]
  P -->|Approve| R[Transaction: claim APPROVED, post CLAIMED]
  R --> S[Create temporary ACTIVE allocation for volunteer]
  S --> T[Volunteer notified of assignment via email and in-app]

Operational philosophy & relationship

Toodle separates general semester-long staffing from session-level attendance variations and emergency assistance:

  1. Course Applications (/courses/:id/applications): The formal, semester-long recruitment pipeline where students submit academic marks for organiser verification, declare availability, and get appointed as course tutors.
  2. Excusals (/excusals): The mechanism for appointed tutors to request planned or emergency leave from individual scheduled sessions of their active allocations.
  3. Overflow Volunteering (/overflow-posts and /overflow-claims): The marketplace for short-term, modular, or surge teaching capacity. It handles both:
    • Emergency absence coverage: When an organiser approves an excusal, Toodle automatically generates an open overflow post for that specific course and session date.
    • Manual staffing surge: Organisers can create ad-hoc overflow posts whenever additional lab or tutorial assistance is required without altering baseline course allocations.

Crucially, overflow volunteering allows both existing tutors (with spare weekly capacity) and qualified students to volunteer without re-running the full course application process.


Tutor excusal walkthrough

  1. Sign in with an authenticated account holding the TUTOR role. Navigate to Excusals (/excusals) from the sidebar.
  2. The page loads your personal excusal history with status badges (Pending, Approved, Declined).
  3. Click + Request excusal to open the request dialog:
    • Course: Select from your active allocations. Allocations in PENDING or removed states are excluded.
    • Session date and time: Select the future date and time of the session you cannot attend. Retroactive dates are rejected server-side with 422 Unprocessable Entity.
    • Reason: Provide a clear explanation for the absence.
  4. Choose Send request. The request is submitted to POST /api/v1/excusals.
  5. The server verifies:
    • Caller owns the specified allocation (403 Forbidden if mismatched).
    • Allocation status is ACTIVE (422 Unprocessable Entity if pending or inactive).
    • Session date is strictly in the future (422 Unprocessable Entity if in the past).
    • No existing excusal already exists for the same allocation and session date (409 Conflict if duplicated).
  6. Once submitted, the request appears in your excusal list with a yellow Pending badge.
  7. When the organiser resolves the request, you receive an in-app notification and an automated email via Brevo indicating whether the request was approved or declined (with the organiser's reason).

Organiser excusal review walkthrough

  1. Sign in with an authenticated account holding the ORGANISER role and navigate to Excusals (/excusals).
  2. Organisers see all pending excusal requests across all courses, displaying tutor name, course code, session date, and submitted reason.
  3. For each request:
    • Approve: Atomically transitions the excusal status from PENDING to APPROVED, records the reviewer ID and resolution timestamp, and automatically generates an open OverflowPost for the course with the description: "Cover needed: <CourseCode> on <Date> (excusal approved for <TutorName>)". An email notification and an in-app notification are dispatched to the excused tutor.
    • Decline: Opens a confirmation dialog requesting an optional or mandatory explanation. Transitions status to DECLINED, records the reviewer ID, resolution timestamp, and feedback reason, and notifies the tutor.
  4. Concurrency guard: If another organiser resolves the request concurrently, the second resolution attempt safely fails with 409 Conflict ("Excusal is no longer pending").

Volunteer overflow walkthrough (Student / Tutor)

  1. Sign in as an authenticated student or tutor and navigate to Volunteers (/volunteers).
  2. The page displays all currently OPEN overflow posts. For non-organisers, posts that have already been claimed or closed are filtered out server-side.
  3. Each post card details:
    • Course code and name.
    • Description of work needed (e.g. covering an excused lab session or additional grading support).
    • Hours required (hoursNeeded).
    • Date posted and organiser name.
  4. Click Volunteer for this slot on an open post.
  5. The server enforces strict capacity and uniqueness checks:
    • Post Openness: The post must have status OPEN (422 Unprocessable Entity if already claimed).
    • No Double Claims: The user must not have already submitted a claim for this post (409 Conflict enforced by composite unique index [overflowPostId, userId]).
    • Weekly Hours Budget: The server calculates the user's current allocated workload across all active allocations and checks: $$\text{usedHours} + \text{post.hoursNeeded} \le \text{user.maxHoursPerWeek}$$ If the requested slot would cause the volunteer to exceed their permitted weekly hours, the server rejects the claim with 422 Unprocessable Entity reporting available vs needed hours.
  6. Upon successful claim, an OverflowClaim record is created with status PENDING.

Organiser overflow management walkthrough

  1. Sign in as an ORGANISER and navigate to Volunteers (/volunteers).
  2. Manual Post Creation: Click + Post overflow work. Select the course, specify hours needed (minimum 1 hour), enter description, and submit.
  3. Broadcasting: Creating a post automatically queries all active STUDENT accounts and sends transactional email and in-app notifications announcing the volunteer opportunity.
  4. Reviewing Claims: The Organiser view displays an approval queue for pending claims showing the claimant's name, email, role, date claimed, and hours required.
  5. Approving a Claim: Click Approve claim. This executes an atomic PostgreSQL transaction:
    • Verifies the claim is still PENDING and the post is still OPEN.
    • Updates OverflowClaim.status to APPROVED, setting reviewedById and approvedAt.
    • Updates OverflowPost.status to CLAIMED.
    • Creates a temporary Allocation for the volunteer with status: ACTIVE, reason: "Volunteer cover for overflow", and hoursPerWeek: post.hoursNeeded.
    • Dispatches a transactional email and in-app notification to the volunteer confirming their new active allocation.
  6. Deleting a Post: An organiser can delete an unneeded overflow post via DELETE /api/v1/overflow-posts/:id. The server prevents deletion if any claim on that post has already been approved (422 Unprocessable Entity).

Concurrency & database integrity

The excusal and overflow services utilise PostgreSQL transactions (prisma.$transaction) and optimistic concurrency guards to prevent race conditions and duplicate staffing:

// Atomically approve excusal and spawn overflow post
const result = await prisma.$transaction(async (tx) => {
    const transition = await tx.excusal.updateMany({
        where: { id: excusalId, status: "PENDING" },
        data: {
            status: "APPROVED",
            reviewedById: reviewerId,
            resolvedAt: new Date(),
        },
    });

    if (transition.count !== 1) {
        throw new ConflictError("Excusal is no longer pending");
    }

    const overflowPost = await tx.overflowPost.create({
        data: {
            courseId: excusal.allocation.courseId,
            description: `Cover needed: ${excusal.allocation.course.code} on ${date} (excusal approved for ${excusal.user.name})`,
            hoursNeeded: excusal.allocation.hoursPerWeek,
            createdById: reviewerId,
        },
    });

    return { excusal: updated, overflowPost };
});

Key database constraints include:

Model Constraint / Index Purpose
Excusal allocationId FK ON DELETE CASCADE Ensures excusal is tethered to a valid tutor assignment
Excusal Unique application check [allocationId, sessionDate] Prevents multiple excusals for the same session slot
OverflowPost courseId FK ON DELETE CASCADE Cascade deletion when course is removed
OverflowClaim @@unique([overflowPostId, userId]) Enforces single claim per user per post at database engine level
Allocation @@unique([userId, courseId]) Prevents duplicate course allocations

API contract

All endpoints require authentication (Authorization: Bearer <token>) and are prefixed with /api/v1.

Method Endpoint Allowed Roles Description Status Codes
GET /excusals Any authenticated Tutors see own excusals; Organisers see all (filterable by ?userId=, ?status=, ?courseId=) 200, 401
POST /excusals TUTOR Request leave from an active session. Body: { allocationId, sessionDate, reason } 201, 400, 403, 409, 422
POST /excusals/:id/approve ORGANISER Approve excusal and auto-create linked OverflowPost 200, 401, 403, 404, 409, 422
POST /excusals/:id/decline ORGANISER Decline excusal with optional reason. Body: { reason? } 200, 400, 401, 403, 404, 409, 422
GET /overflow-posts Any authenticated Students/tutors see OPEN posts; Organisers see all 200, 401
POST /overflow-posts ORGANISER Create an overflow post. Body: { courseId, description?, hoursNeeded } 201, 400, 401, 403, 404
DELETE /overflow-posts/:id ORGANISER Delete an unassigned overflow post 200, 401, 403, 404, 422
POST /overflow-posts/:id/claim TUTOR, STUDENT Submit a claim for an open overflow slot 201, 401, 403, 404, 409, 422
POST /overflow-claims/:id/approve ORGANISER Approve claim, mark post claimed, and create volunteer allocation 200, 401, 403, 404, 409, 422

Acceptance script

Execute this role-based journey using test accounts in a staging or development environment:

  1. Prerequisite Setup:
    • Course exists (e.g. COMS2001A) with Tutor A actively allocated for 4 hours/week.
    • Student B has a registered account with maxHoursPerWeek = 10 and 0 used hours.
    • Organiser C has access to the organiser dashboard.
  2. Tutor Excusal Request:
    • Sign in as Tutor A. Navigate to /excusals.
    • Click + Request excusal. Select COMS2001A, select next Monday's date, and enter "Medical appointment".
    • Submit. Verify card displays with amber Pending badge.
  3. Organiser Excusal Approval:
    • Sign in as Organiser C. Navigate to /excusals.
    • Verify Tutor A's request appears in the queue.
    • Click Approve. Confirm card moves to Approved.
    • Verify in Brevo logs or simulated inbox that Tutor A received an approval email.
  4. Overflow Post Discovery:
    • Navigate to /volunteers. Verify a new open post exists: "Cover needed: COMS2001A on <Date> (excusal approved for Tutor A)" with 4 hours needed.
  5. Student Claim:
    • Sign in as Student B. Navigate to /volunteers.
    • Verify the auto-generated post is visible.
    • Click Volunteer for this slot. Verify claim submits successfully and changes to Pending.
    • Attempt to click claim again; verify button is disabled or rejection occurs.
  6. Organiser Claim Approval:
    • Sign in as Organiser C. Navigate to /volunteers.
    • Find Student B's claim under the claims queue.
    • Click Approve claim.
    • Verify post status changes to Claimed.
    • Navigate to /allocations (Allocation Board) and confirm Student B now has an active allocation for COMS2001A (4 hrs) with reason "Volunteer cover for overflow".
    • Verify Student B received an allocation assignment email.