Parent–Teacher Conferences¶
A conference is a school-wide event on one or more days. Administration creates it as a draft, chooses the participating teachers and their rooms, and publishes it: every participant then receives one appointment slot per grid position, all closed. Teachers open the slots they offer; administration opens booking; referents book a slot with a teacher of their children for one or more of those children. A booking can be cancelled by the referent or by administration; the slot becomes free again and the referent is told in the app.
Canonical implementation: src/conferences/ (one module, three audiences, no row ownership outside the participant and the referent):
| File | Owns |
|---|---|
conference-grid.ts |
Pure appointment grid of a date window and the diff of stored slots against a new grid |
conference-dates.ts |
Pure validation of the dates list and day matching against the stored days |
conference-status.ts |
Pure status machine (CONFERENCE_TRANSITIONS, checkTransition, hasSlots) |
conference-availability.ts |
Pure rule of who may change availability in which status; isFutureSlot |
conferences-admin.controller.ts, conferences-admin.service.ts |
Draft CRUD, grid preview, status commands, participants, rooms, edits with the grid diff |
conferences-teacher.controller.ts, conferences-teacher.service.ts |
The teacher's own conferences and schedule |
conferences-availability.service.ts |
The one availability write behind the teacher route and the admin route |
conferences-family.controller.ts, conferences-family.service.ts |
Eligible teachers, free slots, own bookings, the booking transaction |
conferences-bookings.service.ts |
Cancellation (family and admin) and the admin bookings list |
conferences.queries.ts |
Row locks, eligibility predicates, schedule projection, includes shared by the services |
conferences-{admin,teacher,family}.policy.ts |
Role admission of each audience |
dto/ |
Request and response shapes; conferences.swagger.ts the OpenAPI decorators |
1. Model¶
| Table | Row | Notes |
|---|---|---|
parent_teacher_conferences |
One conference of one academic year | Title (max 200), status, slotDurationMinutes (CHECK > 0), changeoverMinutes (CHECK >= 0), createdByUserId. The year is the active year at creation and never changes |
conference_dates |
One day of a conference | date, startTime/endTime as zero-padded school-local HH:mm text (CHECK end > start); unique per (conference, date) |
conference_participants |
One teacher taking part | Unique per (conference, teacher); availabilitySubmittedAt is the last save of the teacher's own availability |
conference_teacher_rooms |
The room of one participant on one date | Exists only once a room was assigned; no row, or a NULL room (the room FK is SetNull), reads as "not yet assigned" |
conference_slots |
One appointment position of one participant on one date | startTime, endTime, isAvailable (default false); unique per (participant, date, startTime). There is no booked column |
conference_bookings |
One booking of one slot | status CONFIRMED / CANCELLED, the referent, conferenceId and participantId copied from the slot, createdByUserId, cancelledAt, cancelledByUserId |
conference_booking_students |
The students a booking is about | Composite key (booking, student) |
- All seven tables are tenant-bearing with FORCE RLS (Class S) and a
Restricttenant FK; see Multitenancy. Children of a conference, participant or slot cascade on delete. - No database constraint backs the booking rules (one
CONFIRMEDbooking per slot, one per referent and participant, no overlap for a referent). They are enforced by the booking endpoint under the conferenceFOR SHARElock, the referent advisory lock and the slot row lock (§6). Any new path that writes aCONFIRMEDbooking must take the same locks in the same order, or it can create a duplicate the database accepts. - A slot is
BOOKEDwhen aCONFIRMEDbooking holds it. The slot status the API returns (AVAILABLE/UNAVAILABLE/BOOKED) is computed inteacherScheduleSelect(conferences.queries.ts); both the teacher's own schedule and the admin's read of a participant's schedule use that one function. - Participants point at the year's
Teacherrow, so a teacher is identified byteacher.userIdwithin the conference year.
2. Status machine¶
DRAFT ──publish──→ PUBLISHED ──open-booking──→ BOOKING_OPEN ⇄ BOOKING_CLOSED
↑ close-booking │
└──── open-booking ─┘
conference-status.ts is the only place the transitions live. There is no way back to DRAFT, and no status for an ended conference.
| Command | Route | From | To | Extra conditions |
|---|---|---|---|---|
publish |
POST /conferences/:id/publish |
DRAFT |
PUBLISHED |
At least one date (else 409 CONFERENCE_NOT_PUBLISHABLE / NO_DATES), then at least one participant (NO_PARTICIPANTS). Inserts the slots (§3) |
openBooking |
POST /conferences/:id/open-booking |
PUBLISHED, BOOKING_CLOSED |
BOOKING_OPEN |
From BOOKING_CLOSED only while the conference has a slot that has not started (any slot, available or not); else 409 CONFERENCE_NO_FUTURE_SLOTS |
closeBooking |
POST /conferences/:id/close-booking |
BOOKING_OPEN |
BOOKING_CLOSED |
Existing bookings stay |
A command from another status is 409 CONFERENCE_INVALID_STATUS. Every command takes the exclusive conference lock first and re-reads the status under it (§6).
What each status allows:
| Status | Admin edit | Participants and rooms | Availability | Families |
|---|---|---|---|---|
DRAFT |
Title, duration, changeover, dates; delete | Add, remove, set rooms | Not available (no slots) | Cannot see it |
PUBLISHED |
Title always; duration, changeover and dates while no booking is confirmed | Add, remove (while the teacher holds no confirmed booking), set rooms | Teacher and admin | Cannot see it |
BOOKING_OPEN |
As PUBLISHED |
As PUBLISHED |
Teacher and admin | See it, book, cancel own bookings |
BOOKING_CLOSED |
As PUBLISHED |
As PUBLISHED |
Admin only | See it and their bookings; cannot book or cancel |
A draft is deleted by DELETE /conferences/:id (409 CONFERENCE_INVALID_STATUS outside DRAFT); the dates, participants and rooms go with it.
3. Dates and grid¶
Dates¶
dates is a list of { date, startTime, endTime } (at most 60, CONFERENCE_MAX_DATES). On create it is optional; on PATCH /conferences/:id it is the full list: matchConferenceDates matches requested and stored days by calendar day. A day in both keeps its row id (and so its room assignments), a day only requested is inserted, a day only stored is deleted; a kept day whose hours differ is updated in place.
validateConferenceDates returns the first violation, checking each reason over the whole list before the next. The result is 422 CONFERENCE_DATE_INVALID carrying the offending day and one reason:
| Order | Reason | Rule |
|---|---|---|
| 1 | DUPLICATE |
A day appears twice |
| 2 | END_NOT_AFTER_START |
The window ends at or before its start |
| 3 | PAST |
A new day is today or earlier (school-local today) |
| 4 | NO_SLOT_FITS |
The window holds no appointment. Checked for added days, days whose hours changed, and, when duration or changeover change, every stored day after today |
| 5 | PAST_DATE_LOCKED |
A stored day that is today or earlier is removed or has its hours changed |
A stored day that is today or earlier and kept as it is stays out of the NO_SLOT_FITS re-check even when duration or changeover change. An edit with no field is 400 VALIDATION_FAILED; an edit that changes nothing writes nothing and records no audit event.
After publication only the title is free once any booking is confirmed: a change of duration, changeover or the day list first checks the confirmed count and answers 409 CONFERENCE_HAS_CONFIRMED_BOOKINGS, whose message gives the count, before date validation runs. With no confirmed booking the edit is allowed in PUBLISHED, BOOKING_OPEN and BOOKING_CLOSED.
Grid¶
generateDateGrid(window, slotDurationMinutes, changeoverMinutes) walks the window from its start: a slot [start, start + duration) is emitted while it ends at or before the window end, and the next one starts changeover minutes after the previous end. The result is the chronological slots, the changeovers (the gaps between two consecutive slots; none before the first or after the last) and unusedTail (the window left after the last slot, the whole window when no slot fits, null when nothing is left). A non-positive duration generates nothing. generateGrid maps it over the dates and carries any extra field.
GET /conferences/:id/grid-preview returns the grid every teacher gets from the current duration, changeover and dates; nothing is stored. A date with no appointment shows an empty appointments list and the whole window as unusedTail.
Materialization and grid diff¶
Slots exist only from publication on (hasSlots(status)).
- Publish inserts, for every participant and every grid position of every date, one slot with
isAvailable = false(insertSlots). A participant added after publication gets the current grid the same way, closed. - Edit after publication. When duration, changeover or the dates change,
syncSlotsrunsdiffSlots(stored, participantIds, gridByDateId)and applies it in the same transaction. Slots are keyed by(participantId, conferenceDateId, startTime, endTime): a stored slot still in the grid is kept with its availability untouched; a stored slot no longer in the grid, of a participant or day no longer present, or a duplicate of a kept key, is deleted (a cancelled booking on it goes with it); a grid position with no stored slot is inserted closed. A title-only edit never touches slots. - Removing a day deletes its slots and room rows by cascade; a day that keeps its row keeps its room assignments even when its hours move.
4. Admin side¶
All routes are under @Controller('conferences'). Admin writes; the other management presets and SEN Manager read (§8).
| Verb | Path | Scope / grant | Purpose |
|---|---|---|---|
GET |
/conferences |
conferences.setup READ |
Conferences of a year (year query, else the active year; none active returns []), newest first; status filter |
POST |
/conferences |
setup WRITE |
Create a draft in the active year (409 NO_ACTIVE_ACADEMIC_YEAR without one) |
GET |
/conferences/:id |
setup READ |
Detail: dates, participants with the room of every date and their confirmed-booking count |
PATCH |
/conferences/:id |
setup WRITE |
Sparse edit (§3) |
DELETE |
/conferences/:id |
setup WRITE |
Delete a draft |
GET |
/conferences/:id/grid-preview |
setup READ |
Grid preview |
POST |
/conferences/:id/publish, /open-booking, /close-booking |
setup WRITE |
Status commands |
GET |
/conferences/:id/teacher-candidates |
setup READ |
Eligible teachers (departmentId, gradeId, search), each with isParticipant |
POST |
/conferences/:id/participants |
setup WRITE |
Add teachers (teacherIds, 1 to 500) |
DELETE |
/conferences/:id/participants/:participantId |
setup WRITE |
Remove a participant |
PUT |
/conferences/:id/participants/:participantId/rooms |
setup WRITE |
Set the room per date |
GET |
/conferences/:id/participants/:participantId/schedule |
setup READ |
The participant's schedule, shaped as the teacher's own |
PATCH |
/conferences/:id/participants/:participantId/availability |
setup WRITE |
Availability on the teacher's behalf (§5) |
GET |
/conferences/:id/bookings |
conferences.bookings READ |
Bookings by day and time, filters participantId, status, date; every status by default |
POST |
/conferences/:id/bookings/:bookingId/cancel |
action conferences.cancel_booking and setup WRITE |
Cancel a booking (§7) |
Module registration order is an invariant. ConferencesModule registers the family controller, then the teacher controller, then the admin controller. The audience prefixes conferences/family and conferences/teacher must register before the admin conferences/:id/... routes, or their first segment is read as an id. Inside the family controller the static bookings routes likewise come before the :id routes.
Participants. Add is all or nothing and runs in every status: each requested teacher must be eligible (409 CONFERENCE_TEACHER_NOT_ELIGIBLE, naming the ids), then none may already take part (409 CONFERENCE_PARTICIPANT_EXISTS). Remove is refused while that participant holds a confirmed booking (409 CONFERENCE_HAS_CONFIRMED_BOOKINGS, variant PARTICIPANT: the message names the teacher and the count); the participant's rooms and slots go by cascade. An unknown participant, date or slot answers 404 NOT_FOUND.
Rooms. PUT .../rooms takes items[] { conferenceDateId, roomId | null } (each date once). Every date must belong to the conference (404 NOT_FOUND), every room to the conference's academic year (422 CONFERENCE_ROOM_INVALID); null clears the assignment. The response is the participant with the room of every date. An item that changes nothing is not audited.
Eligibility¶
One predicate (eligibleTeacherWhere, conferences.queries.ts) serves the candidates list, the add command, the family reads and the booking transaction. A teacher is eligible when they are ACTIVE in the conference's academic year and, on school-today, teach at least one participating student (status ENROLLED, participatingStudentWhere) of that year through a course: a dated teaching episode on a subject group the student is rostered in, or the combined-class sibling of such a group. It calls teachersTeachingStudentsOn (src/communications/recipient-policy.queries.ts) with includeSupervision: false and includeHomeroomTutor: false: a homeroom tutor who teaches none of the student's courses, and a supervisor of an activity or a break duty, are not teachers of the student here. The predicate is evaluated at each call, so a teacher who stops teaching after publication stays a participant but disappears from a family's view of eligible teachers; bookings already made stay.
5. Availability¶
ConferencesAvailabilityService.update is the single write behind PATCH /conferences/teacher/:id/availability and PATCH /conferences/:id/participants/:participantId/availability. The body is items[] { slotId, isAvailable } (1 to 1000, each slot once); true opens a slot to families, false closes it. The response is the participant's schedule.
Who may write in which status (checkAvailabilityWrite):
| Caller | PUBLISHED |
BOOKING_OPEN |
BOOKING_CLOSED |
DRAFT |
|---|---|---|---|---|
| Teacher (own grid) | yes | yes | 409 CONFERENCE_AVAILABILITY_LOCKED, also for a teacher added after the close |
404 (a draft does not exist for a teacher) |
| Admin (any participant) | yes | yes | yes | 409 CONFERENCE_INVALID_STATUS |
The routine is one transaction: the school-local now is read, the conference is locked FOR SHARE, the participant is resolved (the teacher's own through findOwnParticipant, the admin's by id), the status rule is applied, and the listed slots are locked FOR UPDATE in id order (lockParticipantSlotsForUpdate). All checks run before any write, so one bad slot changes nothing:
- A listed slot that does not exist or is not the participant's is 404
NOT_FOUND(a stranger's slot looks missing). - A slot held by a confirmed booking is 409
CONFERENCE_SLOT_BOOKED; the message lists every such slot as day and times (slots, built from the locked rows, no extra query). - A slot that has already started is 409
CONFERENCE_SLOT_IN_PAST, naming the earliest. A slot is in the future on a later day, or today and starting strictly after the school-local time (isFutureSlot).
Only slots whose value changes are written. A teacher save sets the participant's availabilitySubmittedAt even when nothing changed. An admin save never touches it, and it is audited as conference_availability.updated only when at least one slot changed (§9).
6. Family side and the booking transaction¶
Family routes live under @Controller('conferences/family') and are admitted for the role referent. Rows are fenced to the caller's own Referent (the policy's predicate userId = caller); a caller without one gets [] from the lists and 404 from the single-resource routes.
| Verb | Path | Grant | Purpose |
|---|---|---|---|
GET |
/conferences/family |
conferences.bookings READ |
Conferences in BOOKING_OPEN or BOOKING_CLOSED with at least one teacher of the referent's children (eligibleTeacherCount) |
GET |
/conferences/family/bookings |
bookings READ |
Own bookings by day and time; status (default CONFIRMED), conferenceId |
POST |
/conferences/family/bookings/:bookingId/cancel |
action conferences.cancel_booking |
Cancel an own booking (§7) |
GET |
/conferences/family/:id/teachers |
bookings READ |
The conference's teachers who teach the referent's children, the children each teaches, and the referent's confirmed booking with them; studentId narrows |
GET |
/conferences/family/:id/teachers/:participantId/slots |
bookings READ |
The teacher's free slots by day with the day's room |
POST |
/conferences/family/:id/bookings |
action conferences.book |
Book a slot |
A conference outside BOOKING_OPEN / BOOKING_CLOSED is 404 CONFERENCE_NOT_FOUND for a family. The students considered are the referent's linked students of the conference year who are ENROLLED. The slots route also requires BOOKING_OPEN (409 CONFERENCE_INVALID_STATUS, expected BOOKING_OPEN) and a teacher of the referent's children (404 NOT_FOUND otherwise; unknown and not-yours are one answer). It lists slots that are open, hold no confirmed booking and have not started.
Booking¶
POST /conferences/family/:id/bookings takes { slotId, studentIds } (1 to 20 students, each once). The whole command is one transaction (ConferencesFamilyService.create) that takes three locks in a fixed order and re-checks every rule under them, so a status change, a teacher toggling the slot, another family or a second tab of the same referent cannot slip past a check:
- Conference
FOR SHARE(lockConferenceForShare). The status commands, the edits, the participant commands and the room command take the same rowFOR UPDATE, so they wait for in-flight bookings and are waited for; bookings, availability writes and cancellations hold it shared and run alongside each other. A conference that is missing or not visible to families is 404CONFERENCE_NOT_FOUND. - A per-referent advisory lock
conference-referent:<tenant>:<conference>:<referent>(lockReferentInConference), held to the end of the transaction: two tabs of one referent, or two overlapping slots, serialize. - The slot row
FOR UPDATE(lockSlotForUpdate), scoped to the conference: two referents on one slot, or a teacher toggling it, serialize. A slot not of this conference is 404NOT_FOUND.
The 409 messages of checks 4 to 6 in the table below are rendered from params that are read only after the check fails, inside the same transaction (describeBooking, the teacher lookup): the success path runs no extra query. Chapter 06 owns the rendering (ERROR_TEXT_PARAMS, ERROR_INTERNAL_PARAMS); the ids (bookingId, slotIds, studentIds) stay server-side. A student who is not linked to the caller is never named: when none of the offending students is the caller's, the code carries reason: NOT_LINKED and the message only says that a selected student is not linked to the account. CONFERENCE_SLOT_NOT_AVAILABLE stays generic, because a reason would tell a family that another family just took the slot.
Under the locks, a slot of a teacher who teaches none of the referent's children is 404 NOT_FOUND. Then the checks, in order, each with its own code:
| Order | Check | Error |
|---|---|---|
| 1 | The conference is BOOKING_OPEN |
409 CONFERENCE_INVALID_STATUS |
| 2 | The slot has not started | 409 CONFERENCE_SLOT_IN_PAST |
| 3 | The slot is open and holds no confirmed booking | 409 CONFERENCE_SLOT_NOT_AVAILABLE |
| 4 | The referent holds no confirmed booking with this teacher in this conference | 409 CONFERENCE_BOOKING_TEACHER_TAKEN, the message gives the day and start time of the booking held |
| 5 | None of the referent's confirmed bookings overlaps the slot on its day (start < other.end and end > other.start) |
409 CONFERENCE_BOOKING_OVERLAP, the message gives the day, start time and teacher of the booking in the way |
| 6 | Every named student is linked to the referent and taught by this teacher | 409 CONFERENCE_STUDENT_NOT_ELIGIBLE, the message names the teacher and the referent's own students the teacher does not teach |
Then the booking and its students are inserted and conference_booking.created is recorded. The insert has no unique-violation fallback: the checks above, under the three locks, are the only guard.
7. Cancellation¶
ConferencesBookingsService.cancel is shared by POST /conferences/family/bookings/:bookingId/cancel and POST /conferences/:id/bookings/:bookingId/cancel. It reads the booking unlocked only to learn its conference, locks the conference FOR SHARE, locks the booking FOR UPDATE (lockBookingForUpdate) and re-checks everything under the locks:
| Rule | Family | Admin | Error |
|---|---|---|---|
| The booking exists and is the caller's (family: own referent; admin: in the conference of the route) | yes | yes | 404 CONFERENCE_BOOKING_NOT_FOUND; an admin route with an unknown conference is 404 CONFERENCE_NOT_FOUND |
The conference is BOOKING_OPEN |
required | not required | 409 CONFERENCE_BOOKING_NOT_CANCELLABLE, reason BOOKING_CLOSED |
The booking is CONFIRMED |
required | required | 409 CONFERENCE_BOOKING_NOT_CANCELLABLE, reason CANCELLED |
| The appointment has not started | required | required | 409 CONFERENCE_BOOKING_NOT_CANCELLABLE, reason PAST |
The checks run in the order of the table. A successful cancel keeps the row: status = CANCELLED, cancelledAt, cancelledByUserId. The slot is free again because only CONFIRMED bookings hold a slot; a later booking creates a new row. The response is the booking as the caller sees it (the family shape, or the admin shape with the referent and cancelledByUserId).
In the same transaction the service records conference_booking.cancelled and queues the in-app notification conferences.booking_cancelled for the booking's referent (§9). The notification is sent only after commit, and only when the referent has an active user account.
The admin list GET /conferences/:id/bookings reads the same rows with the referent and who cancelled; an unknown conference is 404, an unknown participantId filter is an empty list.
8. Authorization¶
Each audience is admitted by role through its policy (conferences-*.policy.ts), which @AppliesPolicy composes into @RequireRoles on the handler. Role admission is by role key, not by grant:
| Audience | Roles admitted | Row fence | Grants on top |
|---|---|---|---|
| Admin routes, write | admin (ConferencesAdminPolicy) |
Tenant only: conferences are school-wide | conferences.setup WRITE on every write route; the cancel also needs the action conferences.cancel_booking |
| Admin routes, read | The same policy also admits principal, communications_manager, hr, secretary, administrative_assistant, department_head, curriculum_coordinator and sen_manager |
Tenant only: department_head and curriculum_coordinator read the whole school |
conferences.setup READ on the GET routes; conferences.bookings READ for the bookings list |
| Teacher routes | teacher (ConferencesTeacherPolicy) |
The caller's own ConferenceParticipant in a non-draft conference; anything else is 404 |
conferences.availability READ / WRITE |
| Family routes | referent (ConferencesFamilyPolicy) |
The caller's own Referent and its linked children |
conferences.bookings READ; actions conferences.book, conferences.cancel_booking |
Who reads, who writes. Only admin writes: it alone holds setup WRITE and the two actions. The eight read-only presets hold setup READ and bookings READ (SEN Manager also availability READ) and no action, so each write route answers 403 INSUFFICIENT_SCOPE or ACTION_NOT_PERMITTED, and a read-only caller never changes a row. Teacher, referent, staff and student are not admitted to the admin routes. Grants reach existing tenants through npm run db:reconcile-rbac (chapter 15), which derives them from expectedPresetGrants; there is no data migration.
A custom role that holds conferences.setup is not admitted to the admin routes, read or write: the role allowlist is checked first, so the grant alone is not enough. A caller whose role is not admitted gets 403 ACTION_NOT_PERMITTED.
The three scopes setup, availability and bookings are gate-only (no field filtering). book and cancel_booking are READ-level actions: the referent preset holds bookings READ only, and ownership is the policy's. Presets, conferences.* only:
| Preset | Scopes | Actions |
|---|---|---|
| admin | setup, availability, bookings WRITE |
book, cancel_booking |
| teacher | availability WRITE, bookings READ |
none |
| referent | bookings READ |
book, cancel_booking |
| principal, communications_manager, hr, secretary, administrative_assistant, department_head, curriculum_coordinator | setup READ, bookings READ |
none |
| sen_manager | setup, availability, bookings READ |
none |
| staff, student | none | none |
See Permissions; the catalogue is prisma/seed/rbac-catalogue.ts and the grants prisma/seed/roles.ts.
9. Audit and notification¶
resolveActor once per transaction, one record per logical change (Audit Log):
| Entity type | Actions |
|---|---|
conference |
created, updated (before/after of what changed), deleted, published (date, participant and slot counts), booking_opened, booking_closed (both with from/to), participants_added, participant_removed, participant_rooms_changed (per-date before/after) |
conference_availability |
updated, anchored on the participant: ids of the opened and closed slots. Admin writes that changed at least one slot only; a teacher's own save is not audited |
conference_booking |
created and cancelled, anchored on the booking: conference, participant, teacher, referent, slot, day, times and student ids; cancelled adds cancelledBy (FAMILY / ADMIN) and actorUserId |
Presentation is label-only: the conference has no audit-reader link.
Notification conferences.booking_cancelled (Notifications). Producer: ConferencesBookingsService, once per cancellation, registered inside the transaction and sent after commit with sendAfterCommit. Audience: the booking's referent, when they have an account that is active; no account or a deactivated one is simply not reachable. Both a family cancel and an admin cancel notify the referent. The payload is { bookingId, conferenceId, conferenceTitle, teacherId, teacherName, date, startTime, endTime, cancelledBy } (ConferenceBookingCancelledNotificationPayload in notification.interfaces.ts), target type conference.booking, inbox only: the kind is in NOTIFICATION_IN_APP_ONLY_KINDS and has no email or push specification.
10. Errors¶
Error codes and their parameter shapes are declared in src/common/constants/error-codes.ts (ErrorParamsMap) and their copy in src/common/i18n/error-messages.catalog.ts; the client receives code and messages only (Frontend error contract). Every CONFERENCE_* code is a 409 except CONFERENCE_NOT_FOUND and CONFERENCE_BOOKING_NOT_FOUND (404) and CONFERENCE_DATE_INVALID and CONFERENCE_ROOM_INVALID (422). A reason (CONFERENCE_DATE_INVALID, CONFERENCE_NOT_PUBLISHABLE, CONFERENCE_BOOKING_NOT_CANCELLABLE) selects a more specific catalogue entry. Unknown participants, dates and slots use the generic 404 NOT_FOUND. See Error Handling.
11. Extending¶
- A new status or transition: change
CONFERENCE_TRANSITIONSandhasSlotsinconference-status.ts, then re-read every place that branches on a status: the availability table inconference-availability.ts,FAMILY_VISIBLE_STATUSES, the family booking checks and the cancel rule. - A new slot rule (a cap per teacher, another reason to refuse a booking): add the check to
ConferencesFamilyService.createafter the locks, with its own code. No database constraint backs the rule, so the locks are what make it hold. - A new write that must not race a booking: lock the conference
FOR UPDATEif it changes the conference, grid or participants,FOR SHAREif it must run alongside bookings, and take slot locks in id order. - Conferences import other modules only through their barrels and the shared recipient predicate; no owner module imports conferences.