HandbookOperations23

Boarding life.

The school's other half. The teacher's classroom shapes the academic side; the warden's hostel shapes everything else — sleep, meals, behaviour after lights-out, the moment a boarder leaves for the weekend, the moment she comes back. YESS treats Boarding as a system, not a folder: roll-call writes attendance, leave requests notify parents, curfew breaches ping phones, complaints escalate to admin, maintenance resolutions post to the ledger. Every operation a warden performs has a downstream consequence the rest of the platform can see — and the platform has fixed every silent failure it shipped with.

  • 13tables
  • 7cross-cluster fanouts
  • 3production bugs caught + fixed
  • EN/FR/ARevery surface

Prologue

What a warden's week actually looks like#

Sunday 16:00, the warden opens her dashboard at /dashboard/hostel. Six leave requests came in over the weekend; she approves four, rejects two — the trigger fans a notification to each parent within the transaction. A new boarder's room assignment was created Friday; the gender-match trigger accepted it (mixed-gender hostels are configured per-school). By Monday 06:30 the morning roll-call runs on the warden's phone: she taps "absent" for one boarder; the bridge writes a `student_attendance` row so the form tutor sees it before first period.

21:30 — curfew. The mobile RPC marks one boarder late, two absent. The notify trigger walks each affected parent's profile and fires a notification ("CURFEW ABSENT:" with high priority). That trigger was silently broken from September 2024 to May 2026 — wrong column names on the notifications insert AND wrong column in the parent join. Migration 00653 fixed it; parents now actually get the messages.

Wednesday — a complaint. A boarder flags broken Wi-Fi as 'maintenance'. The warden marks it 'escalated' (the school's ISP is the real culprit); the trigger pings every staff member with `hostel.manage` permission. By Thursday the fault is fixed and the bursar marks the request resolved with an actual_cost. The maintenance→finance trigger posts the journal entry (DR 5500 Hostel maintenance / CR 1100 Cash) within the same transaction the resolve happened in.

Thirteen chapters. One residential life. Read this one first.

Chapter 1

The thirteen tables#

Boarding's schema covers every step from the warden's first building setup to the boarder's daily curfew check. Thirteen tables across four migrations (00080, 00570, 00573, 00580, 00583), all RLS-scoped to public.get_school_id().

  • Physical infrastructure — hostels (per-school dormitory, gender + warden + capacity), hostel_rooms (floor + room number + capacity), hostel_beds (per-physical bed).
  • Assignments + fees — hostel_assignments (student → bed, with gender-match trigger from 00570), hostel_fee_structures (per-hostel fee + frequency).
  • Daily ops — hostel_roll_calls (morning + evening + lights-out roll, per-student JSONB array), hostel_curfew_settings + hostel_curfew_checks (per-student-per-night curfew tracking with cron seed), hostel_meal_plans (links to canteen).
  • Lifecycle events — hostel_leave_requests (pending → approved / rejected → returned with parent comm fanout), hostel_visitors + hostel_visitor_preapprovals (parent pre-approves before the visit so the guard knows who's coming).
  • Quality + grievance — hostel_inspections (periodic room/dorm inspection with score), hostel_complaints (student grievance log with escalation routing), hostel_maintenance_requests (broken bed / tap / bulb with auto-post to finance ledger on resolve).
  • Config — hostel_settings (per-school config).

Chapter 2

Gender match at the assignment#

Schools that run gendered hostels (most do) need a guarantee that no student ends up in the wrong dorm by data-entry error. The schema enforces it at the database, not the UI.

On every INSERT or UPDATE of hostel_assignments where either hostel_id or student_id changes, the BEFORE trigger enforce_hostel_gender_match (00570) reads the hostel's gender setting and the student's gender. If they conflict, the trigger raises an exception and the assignment never commits. Mixed-gender hostels are configurable — schools that don't enforce gender separation set hostels.gender='mixed' and the trigger short-circuits.

Leave-request lifecycle

Boarder asks for weekend leave

  1. pending — student submits
    • Student opens the portal, picks dates + leave_type (weekend / holiday / emergency / medical / family / other) + reason + pickup person.
    • Row INSERTed with status='pending', parent_consent=NULL.
  2. approved — warden signs off
    • Warden taps Approve. Status flips to 'approved', approved_by + approved_at stamped.
    • BEFORE UPDATE trigger trg_hostel_leave_request_notify (00655 Phase B) fans a parent notification: title 'Leave approved' + body with dates + leave_type.
    • Multi-hop join: parent_student_links → parents → user_profile_id.
  3. rejected — warden declines
    • Status flips to 'rejected'.
    • Same trigger fires: parent notification with 'declined' message + pointer to warden contact.
  4. returned — boarder back at hostel
    • Warden taps Returned; actual_return_date stamped.
    • Trigger fires: parent notification 'Returned to hostel on <date>'.
    • parent_notified + parent_notified_at idempotency stamp ensures only one notification per status flip.

Parent sees return confirmation

Chapter 4

Roll-call feeds attendance#

The marketing claim "Hostel roll call feeds attendance — boarders never go missing" was false in production from launch through May 2026. No trigger existed. The hostel_roll_calls table stored per-student absences in a JSONB array; nothing wrote to student_attendance. The form tutor saw a missing boarder for the first time in first period — too late.

Migration 00655 Phase A fixed it. The trigger hostel_roll_call_feed_attendance fires AFTER INSERT or UPDATE OF completed_at / records on hostel_roll_calls. It:

  1. Skips non-morning periods (evening + lights_out are residential, not academic).
  2. Skips incomplete roll-calls (no completed_at).
  3. Walks the records JSONB array; for each entry with status='absent', resolves the student's current section_id from students.section_id.
  4. INSERTs student_attendance (status='absent') ON CONFLICT (student_id, date) DO NOTHING.

The ON CONFLICT clause matters: if the class teacher beat the warden to it (rare — morning roll runs before first period), the teacher's mark wins. If the warden marks first, the teacher's later mark replaces ours via the UNIQUE constraint update path. Either way: a missing boarder appears on the attendance officer's morning queue in real time, not in the afternoon when somebody finally notices.

Chapter 5

Curfew (with the trigger that actually fires#

Configured at /dashboard/hostel/settings: weeknight_time, weekend_time, grace_minutes, auto_create_checks, notify_parent_on_late, notify_parent_on_absent. The cron run_hostel_curfew_seed auto-creates one hostel_curfew_checks row per assigned boarder per night.

At curfew the warden taps each student on the mobile RPC hostel_curfew_mark(p_check_id, p_status, p_notes, p_excuse_reason). Status flips to on_time / late / absent / excused.

Chapter 6

Complaints + escalation routing#

A boarder lodges a complaint with category maintenance / plumbing / electrical / pest / noise / theft / bullying / food / other, severity low/medium/high/critical, and a free-text description. Status starts at open; warden transitions to in_progress / resolved / closed / escalated.

Migration 00655 Phase C ships the comm fanouts. On escalated: hostel_complaint_notify trigger walks the role chain (user_role_assignments → roles → role_permissions → permissions) and writes a notification to every user with hostel.manage permission. On resolved with a student_id attached, the complainant student gets a "Your complaint was resolved" ping in their portal.

Chapter 7

Maintenance posts to the ledger#

A maintenance request hits /dashboard/hostel/maintenance with category + severity + photos. Bursar assigns staff, tracks estimated_cost, and on resolution stamps actual_cost + resolution_notes.

Migration 00655 Phase D wires the finance journal. The trigger hostel_maintenance_post_on_resolve fires AFTER UPDATE OF status, actual_cost on hostel_maintenance_requests. When status flips to 'resolved' AND actual_cost > 0 AND expense_id IS NULL, it calls the SECURITY DEFINER RPC post_hostel_maintenance_expense:

  1. Find-or-create the HOSTEL-MAINT expense category.
  2. INSERT expenses row scoped to the request.
  3. Post journal entry — debit 5500 Hostel maintenance, credit 1100 Cash — via the standard c4_post_journal_entry helper.
  4. Stamp expense_id back on the maintenance request row.

Idempotent: re-resolve doesn't double-post (the trigger guards on expense_id IS NULL; the RPC also re-checks). Mirror of post_transport_fuel_expense (00564) and post_library_acquisition_expense (00569) — the cluster's third atomic ledger fanout.

Chapter 8

Visitor pre-approvals at the gate#

The pre-approval flow (migration 00580) lets parents register intended visits before they arrive. The guard at /dashboard/hostel/visitor-preapprovals checks the pre-approval list when a visitor arrives; recognised visitors get expedited check-in, unknown ones require a warden call before entry.

Walk-in visitors (no pre-approval) get logged in /dashboard/hostel/visitors. The two surfaces share the same recipient join. Overstay escalation (visitor stayed past expected_check_out) is schema-supported but the trigger isn't wired yet — flagged for a future fix-batch.

Chapter 9 · prologue

The canteen runs on the same contracts#

Boarding doesn't end at the bed. The canteen feeds every boarder three times a day plus the day-students who buy lunch, runs an inventory that's a real warehouse, posts revenue + COGS to the ledger in the same transaction the POS tap commits, and (since migration 00656) cross-checks every order against the student's health profile. The canteen shares the boarding cluster's three discipline gates: every state change writes a downstream signal, every count on a screen is a trigger-maintained denorm, and every flagged incident pings the right person within the transaction.

Open the canteen surface at /dashboard/canteen. Five sub-pages cover the operational day: menu (/dashboard/canteen/menu), daily menus, orders, transactions, and recipes. Two more handle administration: meal plans + subscriptions. Three cover finance + reporting: inventory, transactions, reports.

Chapter 10

The canteen's thirteen tables#

Canteen's schema covers everything from menu config to the journal entry. Thirteen tables across three migrations (00081, 00568, 00585). Note the symmetry with Hostel — another thirteen-table sub-domain. Both are residential.

  • Menu + recipe — canteens (per-school outlet), canteen_menu_items (per-item with allergens + dietary_tags as JSONB on the item itself), canteen_daily_menus (what's being served today), canteen_recipes + canteen_recipe_ingredients (dish spec with cost-per-serving + produce RPC that decrements inventory).
  • Subscription + POS — canteen_meal_plans (plan definitions), canteen_meal_subscriptions (per-student enrolment with auto-renewal cron), canteen_orders (the POS log with items JSONB + payment_method + dietary_flag JSONB columns from 00656), canteen_transactions (financial ledger sibling).
  • Inventory + reports — canteen_inventory (raw stock), canteen_inventory_movements (in/out with auto-COGS trigger), canteen_operator_reports (daily summary), canteen_settings (per-school config).

Chapter 11

Dietary cross-check at the database#

The marketing eliteCloser "Canteen dietary cross-check (allergies, halal, vegetarian) flags incompatible meals" was false from launch through May 2026. No trigger existed. Schools that had configured student_health_profile.food_allergens + dietary_restrictions got nothing back — orders went through unflagged.

Migration 00656 fixed it. Three phases in one transaction:

  1. Phase A — schema columns. canteen_orders.dietary_flag JSONB + dietary_flagged_at TIMESTAMPTZ + partial index on flagged orders.
  2. Phase B — BEFORE INSERT trigger canteen_order_dietary_check. Skips staff/guest orders (only students have a health profile). Walks items[], looks up each canteen_menu_items.allergens + dietary_tags (flattened from JSONB to TEXT[] via jsonb_array_elements_text), intersects with student_health_profile.food_allergens via SET INTERSECT, computes restriction conflicts via SET EXCEPT (student has 'halal' restriction but item lacks 'halal' tag → conflict). Stamps dietary_flag JSONB with per-item breakdowns when any conflict found. Does NOT block the order — operator decides after talking to the student.
  3. Phase C — AFTER INSERT trigger canteen_order_flag_notify. Fires when dietary_flagged_at is set. Multi-hop parent join (the same parent_student_links → parents → user_profile_id walk that powers Transport, Hostel, Infirmary). Writes one notification per parent. EXCEPTION-wrapped so a comm-hub failure cannot poison the order insert.

Chapter 12

The canteen money flow#

Three triggers from migration 00568 wire the canteen into the school's ledger:

  • Order → Finance. canteen_order_revenue_post_trigger fires AFTER INSERT on canteen_orders; posts the total_amount to revenue via c4_post_journal_entry. Whether the payment method is cash, mobile money, card, wallet, meal_plan, or invoice, the journal entry lands within the same transaction the POS tap committed in.
  • Inventory movement → COGS. canteen_inventory_movement_post trigger calls post_canteen_inventory_cogs on every stock-out (recipe production, waste, transfer). Posts a journal entry that increments COGS by the movement's value.
  • Hostel assignment → meal plan auto-enrol. auto_enrol_boarder_meal_plan trigger fires on hostel_assignments INSERT — every new boarder gets enrolled in the school's default meal plan automatically. The bursar doesn't double-enter.

A fourth piece — the subscription renewal cron run_canteen_subscription_renewal — walks every expiring subscription nightly and creates the next period's row + the next invoice line. The bursar wakes up with tomorrow's billing already done.

Chapter 13

Recipes + the produce RPC#

A recipe is a dish spec (cost-per-serving, allergens, dietary tags, prep time) plus a list of ingredients with per-serving quantities. The chef opens /dashboard/canteen/recipes, defines the recipe, attaches ingredients from the inventory catalogue.

When the kitchen produces N servings, the chef calls the RPC canteen_recipe_produce(p_recipe_id, p_servings, p_notes). The RPC walks the recipe's ingredients, INSERTs one canteen_inventory_movements row per ingredient with quantity = qty_per_serving × N. The inventory-COGS trigger then posts each ingredient's value to the ledger. One tap = real-time stock decrement + real ledger entry.

Permissions

Who can do what#

Hostel side

  • hostel.view — read every surface. Default for school admins, ops managers, wardens, and (scoped) students + parents for their own records.
  • hostel.create — assign students to beds, log visitors, file maintenance requests. Default for wardens.
  • hostel.edit — update assignments, approve leave, mark curfew, resolve complaints, update maintenance. Default for wardens.
  • hostel.manage — configure settings, manage fee structures, schedule curfew, run manual seed. Default for school admins. Used by the complaint escalation trigger to identify recipients of escalated issues.
  • hostel.delete — hard delete buildings / rooms / beds. Default for school admins only.

Canteen side

  • canteen.view — read every surface. Default for ops, canteen managers, and (scoped) students + parents for their own orders + transactions.
  • canteen.create — place orders, run recipe production (which fires the COGS journal trigger). Default for canteen operators.
  • canteen.edit — cancel orders, refund, update inventory counts. Default for canteen operators.
  • canteen.manage — manage recipes + ingredient catalogue, configure meal plans + subscriptions, edit canteen settings. Default for canteen managers.
  • canteen.delete — hard delete menu items + recipes. Default for school admins.

What makes this elite

  1. 01

    Gender match enforced at the database database

    Schools with gendered hostels can't accidentally assign a girl to the boys' wing. enforce_hostel_gender_match trigger reads hostels.gender + students.gender and raises before the row commits. Mixed-gender hostels opt-in via hostels.gender='mixed'.

  2. 02

    Roll-call feeds attendance — for real for real

    Morning roll-call writes student_attendance for every absent boarder. Form tutor sees the gap before first period. ON CONFLICT respects the teacher's later in-class mark. Was missing entirely until 00655 — now correct.

  3. 03

    Leave-request fanouts parents — for real for real

    approved / rejected / returned each writes a notification per parent via the canonical multi-hop join. parent_notified + parent_notified_at stamped idempotently. Was missing entirely until 00655.

  4. 04

    Curfew notifies parents — for real for real

    late / absent transitions ping every parent. Was silently broken from 00583 (Sep 2024) through 00653 (May 2026) — wrong column names on notifications + wrong parent join. 20 months of silent failures fixed.

  5. 05

    Complaint escalation routes to hostel.manage holders routes

    On 'escalated', the trigger walks user_role_assignments → roles → role_permissions → permissions to find every staff with hostel.manage. Resolve sends a thanks to the complainant student.

  6. 06

    Maintenance resolve posts the journal entry posts

    post_hostel_maintenance_expense RPC + AFTER UPDATE trigger. On resolve with actual_cost > 0: HOSTEL-MAINT category, expense row, journal entry (DR 5500 / CR 1100). Idempotent. Mirror of transport fuel + library acquisition.

  7. 07

    Shop RLS — parents see their child's orders see

    Was silently broken from 00563 (wrong column on parent_student_links). Staff with shop.view always saw everything; parents saw their own orders only. 00653 rewrote the RLS with the canonical parent walk. Parents now see their child's orders.

  8. 08

    Canteen dietary cross-check — at the trigger, not the form trigger

    BEFORE INSERT trigger walks items[], intersects canteen_menu_items.allergens with student_health_profile.food_allergens, computes restriction conflicts via SET EXCEPT. Stamps dietary_flag + dietary_flagged_at, fires parent notification in the same transaction. Was marketed but never wired until 00656; now real.

  9. 09

    Hostel boarder → canteen meal plan auto-enrolment auto

    auto_enrol_boarder_meal_plan trigger from 00568 fires on hostel_assignments INSERT — every new boarder lands on the school's default meal plan. Bursar doesn't double-enter; subscription renewal cron picks them up nightly.

  10. 10

    Canteen order → revenue + COGS in the same transaction same transaction

    canteen_order_revenue_post + canteen_inventory_movement_post triggers wire the POS tap straight to the ledger. Revenue lands when the order INSERTs; COGS lands when the recipe production INSERTs the inventory_movements row. No nightly batch.