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:
- 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. - Excusals (
/excusals): The mechanism for appointed tutors to request planned or emergency leave from individual scheduled sessions of their active allocations. - Overflow Volunteering (
/overflow-postsand/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¶
- Sign in with an authenticated account holding the
TUTORrole. Navigate to Excusals (/excusals) from the sidebar. - The page loads your personal excusal history with status badges (
Pending,Approved,Declined). - Click + Request excusal to open the request dialog:
- Course: Select from your active allocations. Allocations in
PENDINGor 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.
- Course: Select from your active allocations. Allocations in
- Choose Send request. The request is submitted to
POST /api/v1/excusals. - The server verifies:
- Caller owns the specified allocation (
403 Forbiddenif mismatched). - Allocation status is
ACTIVE(422 Unprocessable Entityif pending or inactive). - Session date is strictly in the future (
422 Unprocessable Entityif in the past). - No existing excusal already exists for the same allocation and session date (
409 Conflictif duplicated).
- Caller owns the specified allocation (
- Once submitted, the request appears in your excusal list with a yellow
Pendingbadge. - 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¶
- Sign in with an authenticated account holding the
ORGANISERrole and navigate to Excusals (/excusals). - Organisers see all pending excusal requests across all courses, displaying tutor name, course code, session date, and submitted reason.
- For each request:
- Approve: Atomically transitions the excusal status from
PENDINGtoAPPROVED, records the reviewer ID and resolution timestamp, and automatically generates an openOverflowPostfor 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.
- Approve: Atomically transitions the excusal status from
- 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)¶
- Sign in as an authenticated student or tutor and navigate to Volunteers (
/volunteers). - The page displays all currently
OPENoverflow posts. For non-organisers, posts that have already been claimed or closed are filtered out server-side. - 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.
- Click Volunteer for this slot on an open post.
- The server enforces strict capacity and uniqueness checks:
- Post Openness: The post must have status
OPEN(422 Unprocessable Entityif already claimed). - No Double Claims: The user must not have already submitted a claim for this post (
409 Conflictenforced 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 Entityreporting available vs needed hours.
- Post Openness: The post must have status
- Upon successful claim, an
OverflowClaimrecord is created with statusPENDING.
Organiser overflow management walkthrough¶
- Sign in as an
ORGANISERand navigate to Volunteers (/volunteers). - Manual Post Creation: Click + Post overflow work. Select the course, specify hours needed (minimum 1 hour), enter description, and submit.
- Broadcasting: Creating a post automatically queries all active
STUDENTaccounts and sends transactional email and in-app notifications announcing the volunteer opportunity. - Reviewing Claims: The Organiser view displays an approval queue for pending claims showing the claimant's name, email, role, date claimed, and hours required.
- Approving a Claim: Click Approve claim. This executes an atomic PostgreSQL transaction:
- Verifies the claim is still
PENDINGand the post is stillOPEN. - Updates
OverflowClaim.statustoAPPROVED, settingreviewedByIdandapprovedAt. - Updates
OverflowPost.statustoCLAIMED. - Creates a temporary
Allocationfor the volunteer withstatus: ACTIVE,reason: "Volunteer cover for overflow", andhoursPerWeek: post.hoursNeeded. - Dispatches a transactional email and in-app notification to the volunteer confirming their new active allocation.
- Verifies the claim is still
- 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:
- Prerequisite Setup:
- Course exists (e.g.
COMS2001A) with Tutor A actively allocated for 4 hours/week. - Student B has a registered account with
maxHoursPerWeek = 10and 0 used hours. - Organiser C has access to the organiser dashboard.
- Course exists (e.g.
- 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
Pendingbadge.
- Sign in as Tutor A. Navigate to
- 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.
- Sign in as Organiser C. Navigate to
- 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.
- Navigate to
- 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.
- Sign in as Student B. Navigate to
- 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 forCOMS2001A(4 hrs) with reason"Volunteer cover for overflow". - Verify Student B received an allocation assignment email.
- Sign in as Organiser C. Navigate to