{"openapi":"3.0.0","info":{"title":"Backend API","version":"0.1.0","description":"API documentation for this service. Use GET /api-docs.json for the full spec as copy-pasteable JSON. All schemas include example values."},"servers":[{"url":"/api"}],"tags":[{"name":"Auth","description":"Authentication — register, login, logout, password reset, and current user"},{"name":"Users","description":"User management — CRUD, search, and role assignment"},{"name":"Roles","description":"RBAC roles and permissions"},{"name":"Health","description":"Service liveness and version"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"ApiSuccessResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"statusCode":{"type":"integer","example":200},"data":{"type":"object","description":"Response payload"},"message":{"type":"string","example":"Data has been successfully retrieved."}},"example":{"success":true,"statusCode":200,"data":{},"message":"Data has been successfully retrieved."}},"ApiSuccessResponseWithPagination":{"type":"object","properties":{"success":{"type":"boolean","example":true},"statusCode":{"type":"integer","example":200},"data":{"type":"array","items":{"type":"object"}},"message":{"type":"string"},"pagination":{"description":"Present only when the client passes `page` or `page_size` query params; omitted when no pagination is requested (all results returned).","type":"object","properties":{"total":{"type":"integer","example":100},"pageIndex":{"type":"integer","example":1},"pageSize":{"type":"integer","example":20}}}},"example":{"success":true,"statusCode":200,"data":[],"message":"Data has been successfully retrieved.","pagination":{"total":100,"pageIndex":1,"pageSize":20}}},"ApiErrorResponse":{"type":"object","properties":{"success":{"type":"boolean","example":false},"statusCode":{"type":"integer","example":401},"data":{"type":"object","nullable":true},"message":{"type":"string"}},"example":{"success":false,"statusCode":401,"data":null,"message":"Invalid or expired authentication token. Please log in again."}},"SearchHit":{"type":"object","required":["id","entity_type","display_name","sub_text"],"properties":{"id":{"type":"string"},"entity_type":{"type":"string","enum":["student","parent","staff","enquiry","invoice"]},"display_name":{"type":"string"},"sub_text":{"type":"string"}}}}},"paths":{"/v1/users":{"get":{"tags":["Users"],"summary":"List users (paginated + searchable)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1},"description":"1-based page index. Omit to return all records."},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":200},"description":"Page size. Defaults to 20 when pagination is requested."},{"in":"query","name":"search","schema":{"type":"string"},"description":"Case-insensitive search over email and full_name."},{"in":"query","name":"is_active","schema":{"type":"boolean"}},{"in":"query","name":"role_code","schema":{"type":"string"}}],"responses":{"200":{"description":"List of users","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"403":{"description":"Missing permission","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/users/{id}":{"get":{"tags":["Users"],"summary":"Get a single user by id","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"User","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Users"],"summary":"Update a user","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"full_name":{"type":"string"},"is_active":{"type":"boolean"},"password":{"type":"string","minLength":8},"role_codes":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"User updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"delete":{"tags":["Users"],"summary":"Soft-delete a user","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"User deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/routes/{id}/delete-impact":{"get":{"tags":["Transport"],"summary":"What deleting this route would remove","description":"Counts the stops and student assignments that would be soft-deleted, so the confirmation dialog can state the consequences up front.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Impact counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/transport/routes/{id}":{"delete":{"tags":["Transport"],"summary":"Soft-delete a route and cascade to stops and student assignments","description":"Marks the route, its stops and its student assignments as deleted. Trip sessions and alerts are retained as history of journeys that actually ran. Returns what was removed.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Counts of the cascaded records","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/transport/routes/{routeId}/stops/reorder":{"put":{"tags":["Transport"],"summary":"Renumber a route's stops to a new visiting order","description":"Accepts the complete ordered list of stop IDs for the route and rewrites sequence_order to 1..N in a single transaction. Used when a stop is inserted mid-route rather than appended. The list must match the route's stops exactly — unknown, duplicated or missing IDs are rejected so the resulting sequence can never have holes or ties.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"routeId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["stop_ids"],"properties":{"stop_ids":{"type":"array","description":"Every stop ID on the route, in the desired order","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"The route's stops in their new order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/transport/staff":{"get":{"tags":["Transport"],"summary":"Transport staff roster (drivers and attendants)","description":"Paginated list of transport staff. Rows are selected by the staff member's transport profile relation, not by designation text, so omitting `kind` returns drivers AND attendants in one combined list.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"kind","schema":{"type":"string","enum":["driver","attendant"]},"required":false,"description":"Restrict to one kind. Omit for both."},{"in":"query","name":"search","schema":{"type":"string"},"required":false,"description":"Matches name, employee code or phone."},{"in":"query","name":"status","schema":{"type":"string"},"required":false},{"in":"query","name":"branch_id","schema":{"type":"string"},"required":false},{"in":"query","name":"page","schema":{"type":"integer","default":1},"required":false},{"in":"query","name":"page_size","schema":{"type":"integer","default":20},"required":false}],"responses":{"200":{"description":"Paginated transport staff","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}},"403":{"description":"Missing m11.driver.view","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"post":{"tags":["Transport"],"summary":"Create a driver or attendant","description":"Creates the staff record (designation derived from `kind`) together with its transport profile in one call, reusing the staff creation path so employee code generation and cache invalidation still apply.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","first_name","last_name","dob","gender","phone","email","joining_date","branch_id"],"properties":{"kind":{"type":"string","enum":["driver","attendant"]},"first_name":{"type":"string"},"last_name":{"type":"string"},"dob":{"type":"string","example":"1985-04-12"},"gender":{"type":"string","enum":["male","female","other"]},"phone":{"type":"string"},"email":{"type":"string"},"joining_date":{"type":"string","example":"2026-07-01"},"branch_id":{"type":"string"},"license_number":{"type":"string"},"license_type":{"type":"string"},"license_expiry":{"type":"string","example":"2030-01-31"},"police_verified":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Created transport staff member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Missing m11.driver.create","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/routes/{id}/measurement":{"put":{"tags":["Transport"],"summary":"Store measured road distance and travel time for a route","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["total_distance_m","est_travel_seconds"],"properties":{"total_distance_m":{"type":"number","example":18700},"est_travel_seconds":{"type":"number","example":2520}}}}}},"responses":{"200":{"description":"Updated route","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/transport/routes/{id}/duplicate":{"post":{"tags":["Transport"],"summary":"Duplicate a route together with its stops","description":"Clones the route and every stop. The copy starts inactive and without a vehicle, so one bus is never double-booked across two routes.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Defaults to \"<source> (Copy)\"."}}}}}},"responses":{"200":{"description":"The newly created route","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/transport/me/route":{"get":{"tags":["Transport — Driver App"],"summary":"My assigned route (driver/attendant own-scope)","description":"Resolves the caller's staff record from the JWT, then returns the route they drive or attend — stops in order, vehicle, and school coordinates. 404 when the caller has no staff record or no route assignment.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"direction","schema":{"type":"string","enum":["morning","evening"],"default":"morning"},"required":false,"description":"Trip direction the app is rendering."}],"responses":{"200":{"description":"The caller's route","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"No staff record / no route assigned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/me/roster":{"get":{"tags":["Transport — Driver App"],"summary":"My student roster grouped by stop (own-scope, current academic year)","description":"Students assigned to the caller's own route for the current academic year, grouped by stop in pickup order. Contains limited student PII (name, class/section, one parent contact number) — gated by driver_app.roster.view.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"direction","schema":{"type":"string","enum":["morning","evening"],"default":"morning"},"required":false}],"responses":{"200":{"description":"Stops with their students","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"No staff record / no route assigned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/me/punch-in":{"post":{"tags":["Transport — Driver App"],"summary":"Punch in — marks the caller on duty for today","description":"Records staff attendance and nothing else — pay is never at risk, so it succeeds on a holiday and with no route assigned. Starting a run is a separate step (POST /transport/me/trips), which is what lets a driver run the morning and the evening trip independently. Any already-running trip on the caller's route is returned in `trip` so the app can resume it. Idempotent — a second tap returns already_punched_in without error, and reopens the day when it had already been punched out.\n","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Attendance recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Caller has no staff record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/me/trips":{"post":{"tags":["Transport — Driver App"],"summary":"Start a run (morning or evening) from Today's Trips","description":"Creates the TripSession for the chosen direction and seeds its stop progress. Either the route's driver or its attendant may start it — whoever taps first becomes the trip's GPS source (started_by_staff_id) and is the only one whose phone may report location. The other crew member receives the same trip with is_gps_owner=false and shows live tracking instead of a Start button. Requires punch-in first (NOT_PUNCHED_IN), refuses on a school holiday (SCHOOL_HOLIDAY) and with no route (NO_ROUTE_ASSIGNED). Re-tapping the running direction returns the same trip; asking for the other direction while one is live is refused with TRIP_ALREADY_RUNNING rather than silently returning the wrong trip.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["direction"],"properties":{"direction":{"type":"string","enum":["morning","evening"]}}}}}},"responses":{"201":{"description":"Trip started (or the already-running trip for this direction)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Not punched in, a holiday, or another direction is already running","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Caller has no staff record or no route assigned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/me/profile":{"get":{"tags":["Transport — Driver App"],"summary":"The caller's own profile — the app's My Profile screen","description":"Own-scope: resolved from the JWT, so there is no id to tamper with and a driver can only ever read themselves. Returns identity, contact, assignment and the compliance dates the staff member should act on, with expiring_soon listing anything inside the same window the document-expiry alert cron uses. Salary (rate_amount / rate_type) and identity-document fields (license_number, aadhaar) are deliberately excluded — this screen lives on a phone that rides in a bus. Licence expiry is included without the licence number.\n","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The caller's transport profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Caller is not transport staff","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Caller has no staff record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/me/duty-today":{"get":{"tags":["Transport — Driver App"],"summary":"Today's duty state — the app's cold-start read","description":"Resolves the caller's duty state from the database rather than the phone, so a reinstall, a new device, or a crash mid-trip resumes correctly. `state` is the single field the UI switches on — not_punched_in, on_duty, or punched_out. When a trip is live it is returned in `trip` and can_punch_out is false, mirroring the punch-out guard so the button is never offered only to be rejected. worked_minutes counts up while on duty and settles once the day is closed.\n","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Duty state for the caller's current school-local day","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Caller has no staff record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/me/punch-out":{"post":{"tags":["Transport — Driver App"],"summary":"Punch out — closes the caller's working day","description":"Records punch_out_time on today's staff attendance row. Independent of trips by design: a driver runs a morning AND an evening trip, so ending a trip must never end the working day. Refused while a trip is still in progress (TRIP_STILL_IN_PROGRESS) and when the day has no punch-in (NOT_PUNCHED_IN). Idempotent — a second tap returns the original punch_out_time with already_punched_out=true rather than extending the day.\n","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Working day closed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Not punched in today, or a trip is still in progress","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Caller has no staff record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/trips/{id}/cancel":{"put":{"tags":["Transport — Driver App"],"summary":"\"Not running today\" — cancel the trip, keep the attendance","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":300}}}}}},"responses":{"200":{"description":"Trip cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not the caller's trip","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Trip not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/trips/{id}/end":{"put":{"tags":["Transport — Driver App"],"summary":"End the trip (blocked while children are still marked on board)","description":"Completes the trip. Fails with EMPTY_BUS_UNCONFIRMED while any student is still marked boarded and the bus has not been confirmed empty — the child-left-on-bus safeguard.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Trip completed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not the caller's trip","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Trip not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/trips/{id}/close":{"put":{"tags":["Transport — Driver App"],"summary":"Confirm the bus is empty and end the trip, in one transaction","description":"Single-call replacement for confirm-empty followed by end. Those two calls leave a partial state when the connection drops between them — empty_confirmed_at written while the trip stays in_progress — which is routine on rural routes and sits on the child-left-on-the-bus path. Both writes happen in one transaction: a refusal writes nothing. Refuses with EMPTY_BUS_UNCONFIRMED while any student is still marked boarded, with students_on_board in the error detail. Idempotent — a retry against an already-completed trip returns that trip rather than TRIP_NOT_ACTIVE. Driver and attendant may both call it (spec §5C); a cancelled trip is not accepted. The older confirm-empty and end endpoints keep working unchanged.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Trip closed — empty confirmed and completed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Students still on board, or the trip is not active","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"403":{"description":"Caller is not this route's driver or attendant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Trip not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/trips/{id}/location":{"put":{"tags":["Transport — Driver App"],"summary":"Report the bus's GPS position (batch of timestamped points)","description":"Restricted to the trip's GPS owner — the crew member who started it (NOT_GPS_OWNER for the other one, NOT_YOUR_TRIP for anyone off the route). One continuous stream per trip keeps the odometer honest. Accepts a batch so the phone can buffer through dead zones and flush when signal returns — each point carries its own recorded_at. Only the newest point advances the live position; out-of-order flushes never move the bus backwards. Each batch also advances the server-side odometer: points are sorted by recorded_at, bridged from the last counted point of the previous batch, and summed — counting only legs between 15 m (below that it is GPS wander from a parked bus) and 2 km (above that it is a glitch or a dead-zone resume). Idempotent: a retried batch is dropped at or before the stored watermark, so the same leg is never counted twice. The response carries the running distance_meters, duration_seconds and avg_speed_kmh so the phone needs no extra round-trip.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["points"],"properties":{"points":{"type":"array","minItems":1,"maxItems":600,"items":{"type":"object","required":["latitude","longitude","recorded_at"],"properties":{"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180},"recorded_at":{"type":"string","format":"date-time"}}}}}}}}},"responses":{"200":{"description":"Position stored; returns the live position after ingest","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Caller is not this trip's driver","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Trip not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/fleet/live":{"get":{"tags":["Transport — Driver App"],"summary":"Live state of every route, with derived alerts (school fleet screen)","description":"One request for the whole fleet — replaces polling /routes/{id}/live once per route. Each row carries the same live fields plus `alert`, which is null when nothing is wrong and otherwise one of: `crew_not_punched_in` (critical — past the first stop's pickup time and NEITHER the driver nor the attendant has punched in; either seat can start the run, so one of them on duty clears it), `trip_not_started` (on duty but no run started), `bus_not_moving` (fresh GPS, no stops reached, still at the origin), or `no_location` (the phone stopped reporting). The last two are mutually exclusive by design: a stale fix means the position is unknown, so `no_location` is worded in the past tense (\"Last seen at X\") and never claims the bus is standing still — a dead zone is normal (spec §11) and must not read as a stalled bus. Each row also carries `driver_punched_in_at` / `attendant_punched_in_at` (per-seat duty status) and `tracked_by_staff_id` / `tracked_by_name` / `tracked_by_role` — whose phone is reporting the position, since either the driver or the attendant may have started the run.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"branch_id","schema":{"type":"string"}},{"in":"query","name":"direction","schema":{"type":"string","enum":["morning","evening"]},"description":"Which run to judge readiness against. Defaults to morning."}],"responses":{"200":{"description":"Every route's live state plus alert_summary counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/transport/routes/{routeId}/live":{"get":{"tags":["Transport — Driver App"],"summary":"Live position of the route's active trip (poll ~10 s)","description":"Viewer side of live tracking for the school fleet screen. Returns active=false when no trip is running. last_location_at lets the client show \"Updated X min ago\" so a signal gap reads as a gap, not a bug. distance_meters, duration_seconds and avg_speed_kmh describe the running trip (all zero when none is active) — this is how the school's web view shows distance at all, having no GPS of its own. next_stop carries distance_m and eta_minutes to the stop the bus is heading for — a straight-line estimate at an assumed average speed (TRANSPORT_AVG_BUS_SPEED_KMPH), not a road-network ETA, so present it as approximate. Both are null until the bus reports a position or when the stop has no coordinates.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"routeId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Live trip state for the route","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Route not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/trips/{id}/stops/{stopId}/reach":{"put":{"tags":["Transport — Driver App"],"summary":"Manually mark a stop reached (attendant tap / driver fallback)","description":"Fallback when GPS is unavailable or wrong (spec §7). The attendant taps freely; the driver's app must only offer this while stationary. Records who marked it. Idempotent — a stop already reached is returned as-is.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"stopId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The stop's progress row after marking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not the caller's trip","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Trip not found / stop not on this trip","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/trips/{id}/progress":{"get":{"tags":["Transport — Driver App"],"summary":"\"Stop 2 of 6\" — per-stop progress of the caller's own trip","description":"Also carries the trip's distance_meters (accumulated server-side from the driver's uploads), duration_seconds and avg_speed_kmh. The last two are derived on read, never stored — a stored average would go stale the moment either input changed. The average spans the whole trip including time parked at stops, so a bus that idled at the kerb reads as slow.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Reached/total counters, next stop, and every stop's status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not the caller's trip","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Trip not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/trips/{id}/attendance":{"post":{"tags":["Transport — Driver App"],"summary":"Bulk mark bus attendance (boarding + evening custody)","description":"One row per student per trip — re-marking flips the row. Statuses: boarded / absent (morning), dropped = handed over, nobody_at_stop (child stays on bus — parent alerted urgently), returned_to_school. Every student and stop must belong to the trip's route. Separate from class attendance. Parent notifications are fire-and-forget.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["entries"],"properties":{"entries":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"object","required":["student_id","stop_id","status"],"properties":{"student_id":{"type":"string"},"stop_id":{"type":"string"},"status":{"type":"string","enum":["boarded","absent","dropped","nobody_at_stop","returned_to_school"]}}}}}}}}},"responses":{"200":{"description":"All marks for the trip after the update, with summary counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not the caller's trip","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Trip not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"get":{"tags":["Transport — Driver App"],"summary":"Current bus-attendance marks for the trip","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Marks + per-status summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not the caller's trip","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Trip not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/trips/{id}/confirm-empty":{"put":{"tags":["Transport — Driver App"],"summary":"Confirm the bus is empty (child-left-on-bus safeguard)","description":"The final human check (spec §8.4/§9): refused with STUDENTS_STILL_ON_BUS while any student is still marked boarded — every child must first be resolved (dropped or returned_to_school). Records empty_confirmed_at, which trip end requires while boarded marks exist. Idempotent.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Trip with empty_confirmed_at set","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not the caller's trip","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Trip not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/transport/routes/{id}/readiness":{"get":{"tags":["Transport"],"summary":"Can this route go live? (stop-coordinate check)","description":"Automatic stop detection compares the bus position against each stop's saved coordinates, so a stop without lat/lng can never fire. This reports whether the route is ready to activate and names the stops that still need coordinates, so the UI can warn before Save instead of surfacing the activation error afterwards.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Readiness with can_go_live, blockers, and the offending stops","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Route not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/timetables":{"get":{"tags":["Timetable"],"summary":"List timetables","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"academic_year_id","schema":{"type":"string"}},{"in":"query","name":"class_id","schema":{"type":"string"}},{"in":"query","name":"section_id","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","finalized"]}}],"responses":{"200":{"description":"Timetable list"}}},"post":{"tags":["Timetable"],"summary":"Create a new timetable (draft)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["academic_year_id","class_id","section_id","branch_id"]}}}},"responses":{"200":{"description":"Created timetable"}}}},"/v1/timetables/{id}":{"get":{"tags":["Timetable"],"summary":"Get timetable by ID (with all slots)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Timetable detail with slots"}}}},"/v1/timetables/{id}/slots":{"post":{"tags":["Timetable"],"summary":"Bulk upsert timetable slots (day x period x subject x teacher)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["slots"],"properties":{"slots":{"type":"array","items":{"type":"object","required":["day_of_week","period_number"],"description":"subject_id and teacher_id are omitted for a Free Period slot"}}}}}}},"responses":{"200":{"description":"Slots upserted"}}}},"/v1/timetables/{id}/finalize":{"post":{"tags":["Timetable"],"summary":"Finalize timetable (validates no teacher conflicts first)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Timetable finalized"},"400":{"description":"Conflict detected"}}}},"/v1/timetables/my":{"get":{"tags":["Timetable"],"summary":"Get the logged-in teacher's own weekly schedule (finalized periods across all classes)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"academic_year_id","schema":{"type":"string"}},{"in":"query","name":"branch_id","schema":{"type":"string"}}],"responses":{"200":{"description":"The caller's own schedule (empty when the account has no staff record)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/timetables/my-classes":{"get":{"tags":["Timetable"],"summary":"What the logged-in user may browse in the timetable Class view","description":"Returns `full` (browse any class), `class_teacher` (locked to the homeroom sections in `sections`), or `subject_teacher` (no Class view).\n","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Class-view access descriptor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/teacher-assignments/me":{"get":{"tags":["Teacher Assignments"],"summary":"Get the logged-in staff member's teaching context (scoped catalog)","description":"Returns the classes, sections, and subjects the logged-in staff member\nteaches in the active academic year, plus the sections where they are\nthe class teacher — as ids (`class_teacher_section_ids`) and with\ndisplay names (`class_teacher_sections`).\n","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Teaching context for the logged-in staff member.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiSuccessResponse"},{"type":"object","properties":{"data":{"type":"object","properties":{"staff_id":{"type":"string","nullable":true},"assignments":{"type":"array","items":{"type":"object"}},"class_ids":{"type":"array","items":{"type":"string"}},"section_ids":{"type":"array","items":{"type":"string"}},"subject_ids":{"type":"array","items":{"type":"string"}},"class_teacher_section_ids":{"type":"array","items":{"type":"string"}},"class_teacher_sections":{"type":"array","description":"Sections where this staff member is the class teacher, with display names.","items":{"type":"object","properties":{"section_id":{"type":"string"},"section_name":{"type":"string"},"class_id":{"type":"string"},"class_name":{"type":"string"}}}}}}}}]}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/teacher-assignments":{"get":{"tags":["Teacher Assignments"],"summary":"List teacher assignments (filterable by teacher, class, subject)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}},{"in":"query","name":"academic_year_id","schema":{"type":"string"}},{"in":"query","name":"staff_id","schema":{"type":"string"}},{"in":"query","name":"class_id","schema":{"type":"string"}},{"in":"query","name":"section_id","schema":{"type":"string"},"description":"When provided, returns assignments for this section plus any class-level (sectionless) assignments."},{"in":"query","name":"subject_id","schema":{"type":"string"}}],"responses":{"200":{"description":"Assignment list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Teacher Assignments"],"summary":"Assign teacher to class + subject + section","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["academic_year_id","staff_id","class_id","subject_id"],"properties":{"academic_year_id":{"type":"string"},"staff_id":{"type":"string"},"class_id":{"type":"string"},"section_id":{"type":"string"},"subject_id":{"type":"string"},"periods_per_week":{"type":"integer"}}}}}},"responses":{"200":{"description":"Assignment created"}}}},"/v1/teacher-assignments/{id}":{"put":{"tags":["Teacher Assignments"],"summary":"Update a teacher assignment","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"staff_id":{"type":"string"},"section_id":{"type":"string","nullable":true},"periods_per_week":{"type":"integer","nullable":true}}}}}},"responses":{"200":{"description":"Assignment updated"}}},"delete":{"tags":["Teacher Assignments"],"summary":"Remove a teacher assignment (soft-delete)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Assignment deleted"},"404":{"description":"Not found"}}}},"/v1/staff/{id}/workload":{"get":{"tags":["Staff","Teacher Assignments"],"summary":"Get teacher workload summary (periods/week, classes, subjects)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Workload summary"}}}},"/v1/substitutions":{"get":{"tags":["Substitution"],"summary":"List substitutions (filterable by date)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"date","schema":{"type":"string","format":"date"}},{"in":"query","name":"original_teacher_id","schema":{"type":"string"}},{"in":"query","name":"substitute_teacher_id","schema":{"type":"string"}}],"responses":{"200":{"description":"Substitution list"}}},"post":{"tags":["Substitution"],"summary":"Create a substitution (absent teacher -> substitute teacher x date)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["original_slot_id","date"],"properties":{"original_slot_id":{"type":"string"},"substitute_teacher_id":{"type":"string"},"date":{"type":"string","format":"date"},"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Substitution created"}}}},"/v1/substitutions/suggest/{date}":{"get":{"tags":["Substitution"],"summary":"Suggest free teachers for a given date","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"date","required":true,"schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Free teacher suggestions sorted by fewest periods"}}}},"/v1/substitutions/teacher-slots":{"get":{"tags":["Substitution"],"summary":"List an absent teacher's substitutable timetable slots for a date","description":"Returns the teacher's finalized-timetable slots for the weekday of the\ngiven date. Slots that already have a substitution for that exact date\nare excluded, so every listed slot is valid for creating a new\nsubstitution (soft-deleted substitutions do not exclude their slot).\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"teacher_id","required":true,"schema":{"type":"string"},"description":"Teacher ID (BigInt as string)"},{"in":"query","name":"date","required":true,"schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Teacher timetable slots ordered by period number","excluding slots already substituted on the date":null}}}},"/v1/substitutions/coverage":{"get":{"tags":["Substitution"],"summary":"Per-teacher substitution coverage summary for a date","description":"One row per teacher with at least one finalized-timetable slot on the\nweekday of the given date: total slots, slots already covered by a\nsubstitution on that exact date, and the remaining uncovered slots.\nLets the frontend hide absent teachers whose every period is already\ncovered (soft-deleted substitutions do not count as coverage).\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"date","required":true,"schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Coverage rows — teacher_id","total_slots":null,"covered_slots":null,"uncovered_slots":null}}}},"/student-remarks":{"get":{"tags":["Student Remarks"],"summary":"List student remarks","description":"Branch-scoped. Callers with `academics.student_remark.view_all` see every remark in the branch; other callers see only remarks they authored plus published remarks in the sections they teach.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"student_id","schema":{"type":"string"}},{"in":"query","name":"class_id","schema":{"type":"string"}},{"in":"query","name":"section_id","schema":{"type":"string"}},{"in":"query","name":"teacher_id","schema":{"type":"string"}},{"in":"query","name":"subject_id","schema":{"type":"string"}},{"in":"query","name":"category","schema":{"type":"string","enum":["behaviour","academic","discipline","attendance","achievement","health","other"]}},{"in":"query","name":"sentiment","schema":{"type":"string","enum":["positive","negative","neutral"]}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","published"]}},{"in":"query","name":"date_from","schema":{"type":"string","format":"date"}},{"in":"query","name":"date_to","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"post":{"tags":["Student Remarks"],"summary":"Add a remark","description":"`branch_id`, `class_id`, `section_id` and `academic_year_id` are derived from the student record — they are never accepted from the body. A `published` remark is visible to the parent immediately; a `draft` is visible only to its author.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["student_id","category","sentiment","title","description"],"properties":{"student_id":{"type":"string"},"subject_id":{"type":"string","nullable":true},"category":{"type":"string","enum":["behaviour","academic","discipline","attendance","achievement","health","other"]},"sentiment":{"type":"string","enum":["positive","negative","neutral"]},"title":{"type":"string"},"description":{"type":"string"},"remark_date":{"type":"string","format":"date"},"status":{"type":"string","enum":["draft","published"]}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Student outside your assigned sections","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/student-remarks/summary":{"get":{"tags":["Student Remarks"],"summary":"Remark counts by sentiment and share status","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"student_id","schema":{"type":"string"}},{"in":"query","name":"class_id","schema":{"type":"string"}},{"in":"query","name":"section_id","schema":{"type":"string"}},{"in":"query","name":"date_from","schema":{"type":"string","format":"date"}},{"in":"query","name":"date_to","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/student-remarks/needs-attention":{"get":{"tags":["Student Remarks"],"summary":"Students accumulating concerns this month","description":"Students with 3 or more published negative remarks in the current month, raised by at least 2 distinct teachers. Principal-only.\n","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/student-remarks/students/{studentId}":{"get":{"tags":["Student Remarks"],"summary":"Full remark timeline for one student","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"studentId","required":true,"schema":{"type":"string"}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}},{"in":"query","name":"category","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/student-remarks/{id}":{"get":{"tags":["Student Remarks"],"summary":"Get one remark","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Student Remarks"],"summary":"Update a remark","description":"Stamps `edited_at`. A published remark cannot revert to draft.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"subject_id":{"type":"string","nullable":true},"category":{"type":"string"},"sentiment":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"remark_date":{"type":"string","format":"date"},"status":{"type":"string","enum":["draft","published"]}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"delete":{"tags":["Student Remarks"],"summary":"Soft-delete a remark","description":"Sets `deleted_at`. The row is retained for audit.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/student-remarks/{id}/comments":{"get":{"tags":["Student Remarks"],"summary":"List comments on a remark","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"post":{"tags":["Student Remarks"],"summary":"Add a comment to a remark","description":"Capped at 5 comments per remark. `author_role` is derived from the session and is never accepted from the body.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["comment"],"properties":{"comment":{"type":"string"}}}}}},"responses":{"201":{"description":"Added","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Comment limit reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/student-remarks/{id}/upload-attachment":{"post":{"tags":["Student Remarks"],"summary":"Attach a file to a remark","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"Uploaded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/students/summary":{"get":{"tags":["Students"],"summary":"Get student counts by status","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"branch_id","schema":{"type":"string"}}],"responses":{"200":{"description":"Student summary counts"}}}},"/v1/students":{"get":{"tags":["Students"],"summary":"List students (paginated, searchable, scope-filtered)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}},{"in":"query","name":"search","schema":{"type":"string"},"description":"Search by name, admission number, or phone"},{"in":"query","name":"class_id","schema":{"type":"string"}},{"in":"query","name":"section_id","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string","enum":["active","transferred","alumni","suspended"]}},{"in":"query","name":"gender","schema":{"type":"string","enum":["male","female","other"]}},{"in":"query","name":"parent_account_status","schema":{"type":"string","enum":["no_phone","none","invited","active"]},"description":"Parent-app account status (only applied when module M15 is active)"},{"in":"query","name":"joined_from","schema":{"type":"string","format":"date"},"description":"Joining-date range start (YYYY-MM-DD, inclusive)"},{"in":"query","name":"joined_to","schema":{"type":"string","format":"date"},"description":"Joining-date range end (YYYY-MM-DD, inclusive)"},{"in":"query","name":"dob_from","schema":{"type":"string","format":"date"},"description":"Date-of-birth range start (YYYY-MM-DD, inclusive)"},{"in":"query","name":"dob_to","schema":{"type":"string","format":"date"},"description":"Date-of-birth range end (YYYY-MM-DD, inclusive)"},{"in":"query","name":"academic_year_id","schema":{"type":"string"}}],"responses":{"200":{"description":"Student list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Students"],"summary":"Create a new student with family info","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["first_name","last_name","dob","gender","branch_id","academic_year_id","class_id","section_id","family"]}}}},"responses":{"200":{"description":"Created student","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/students/{id}":{"get":{"tags":["Students"],"summary":"Get student full profile (with family, academic, documents, custom fields)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Student detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found"}}},"put":{"tags":["Students"],"summary":"Update a student (field sensitivity enforced)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"reason":{"type":"string","description":"Required for edit_with_reason fields"}}}}}},"responses":{"200":{"description":"Updated student","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"delete":{"tags":["Students"],"summary":"Soft-delete a student","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Student deleted"},"404":{"description":"Not found"}}}},"/v1/students/{id}/toggle-status":{"put":{"tags":["Students"],"summary":"Activate or deactivate a student","description":"Sets isActive on the student. Deactivating also sets status to 'inactive'; activating sets status back to 'active'. Blocked for transferred/alumni students.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["is_active"],"properties":{"is_active":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Student activated or deactivated"},"400":{"description":"Already in requested state or status does not allow toggle"},"404":{"description":"Student not found"}}}},"/v1/students/{id}/siblings":{"get":{"tags":["Students"],"summary":"List siblings of a student","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sibling list"}}}},"/v1/students/link-siblings":{"post":{"tags":["Students"],"summary":"Manually link students as siblings","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["student_ids"],"properties":{"student_ids":{"type":"array","items":{"type":"string"},"minItems":2}}}}}},"responses":{"200":{"description":"Siblings linked"}}}},"/v1/students/link-siblings/{linkId}":{"delete":{"tags":["Students"],"summary":"Remove a sibling link","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"linkId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sibling link removed"}}}},"/v1/students/{id}/documents":{"post":{"tags":["Students"],"summary":"Upload a document for a student (multipart/form-data)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["document_type","file"],"properties":{"document_type":{"type":"string"},"file":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"Document uploaded"}}},"get":{"tags":["Students"],"summary":"List documents for a student","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Document list"}}}},"/v1/students/{id}/documents/{docId}/verify":{"put":{"tags":["Students"],"summary":"Verify or reject a student document","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"docId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["verified","rejected"]},"remarks":{"type":"string"}}}}}},"responses":{"200":{"description":"Document verification updated"},"404":{"description":"Document not found"}}}},"/v1/students/{id}/documents/{docId}":{"delete":{"tags":["Students"],"summary":"Soft-delete a student document","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"docId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Document deleted"},"404":{"description":"Not found"}}}},"/v1/students/{id}/erasure-request":{"post":{"tags":["Students"],"summary":"Initiate DPDP erasure for a student","description":"Soft-deletes student and anonymizes all PII fields. Retains transactional records.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Erasure initiated"},"404":{"description":"Student not found"}}}},"/v1/students/{id}/custom-fields":{"put":{"tags":["Students"],"summary":"Bulk upsert custom field values for a student","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"type":"object","required":["definition_id"],"properties":{"definition_id":{"type":"string"},"value_text":{"type":"string","nullable":true},"value_number":{"type":"number","nullable":true},"value_date":{"type":"string","nullable":true},"value_json":{}}}}}}}}},"responses":{"200":{"description":"Custom fields updated"}}}},"/v1/students/{id}/guardian-status":{"get":{"tags":["Students"],"summary":"Guardian phone options + parent-account status for the create-parent dialog","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Guardian options","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/students/{id}/parent-accounts":{"post":{"tags":["Students"],"summary":"Create parent account(s) for one student + send invite","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"numbers":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Provision summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/students/{id}/parent-accounts/welcome":{"post":{"tags":["Students"],"summary":"Resend the welcome message to existing parent account(s) for one student","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"numbers":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Resend summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/students/parent-accounts/bulk":{"post":{"tags":["Students"],"summary":"Queue parent-account provisioning for many students (father+mother default)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["student_ids"],"properties":{"student_ids":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"202":{"description":"Queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/students/parent-accounts/job-status":{"get":{"tags":["Students"],"summary":"Live status of bulk parent-account provisioning for the current school","description":"Reflects actual BullMQ queue state (active/waiting/delayed/etc.) for this tenant. The UI uses `in_progress` to disable the \"add parent accounts\" button until the backend finishes. Fails open ({ in_progress: false, pending: 0 }) if the queue is unreachable so the button is never permanently blocked.\n","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Provisioning status","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiSuccessResponse"},{"type":"object","properties":{"data":{"type":"object","properties":{"in_progress":{"type":"boolean"},"pending":{"type":"integer"}}}}}]}}}}}}},"/v1/staff/{id}/exit":{"post":{"tags":["Staff"],"summary":"Initiate staff exit process","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["notice_date","last_working_date","exit_reason"],"properties":{"notice_date":{"type":"string","format":"date"},"last_working_date":{"type":"string","format":"date"},"exit_reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Exit initiated"}}},"get":{"tags":["Staff"],"summary":"Get exit status with final settlement preview","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Exit status and settlement preview"},"404":{"description":"Exit not initiated"}}}},"/v1/staff/{id}/exit/complete":{"put":{"tags":["Staff"],"summary":"Complete exit process (deactivate login, generate certificates)","description":"Calculates final settlement, deactivates user login, soft-deletes teacher assignments, and optionally attaches experience certificate and relieving letter URLs.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"experience_cert_url":{"type":"string","format":"uri"},"relieving_letter_url":{"type":"string","format":"uri"}}}}}},"responses":{"200":{"description":"Exit completed"}}}},"/v1/staff":{"get":{"tags":["Staff"],"summary":"List staff (paginated, searchable, scope-filtered)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}},{"in":"query","name":"search","schema":{"type":"string"},"description":"Search by name, employee ID, phone, or email"},{"in":"query","name":"status","schema":{"type":"string","enum":["active","on_leave","suspended","exited"]}},{"in":"query","name":"employment_type","schema":{"type":"string","enum":["permanent","contract","visiting","substitute"]}},{"in":"query","name":"department","schema":{"type":"string"}}],"responses":{"200":{"description":"Staff list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Staff"],"summary":"Create a new staff member with optional bank details","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["first_name","last_name","dob","gender","phone","designation","employment_type","joining_date","branch_id"]}}}},"responses":{"200":{"description":"Created staff","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/staff/summary":{"get":{"tags":["Staff"],"summary":"Staff KPI summary — total/active/inactive/on-leave + teacher vs non-teaching counts (scope-filtered)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Staff summary counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/staff/{id}":{"get":{"tags":["Staff"],"summary":"Get staff full profile (with bank details, qualifications, documents)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Staff detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found"}}},"put":{"tags":["Staff"],"summary":"Update a staff member","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated staff","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"delete":{"tags":["Staff"],"summary":"Soft-delete a staff member","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Staff deleted"},"404":{"description":"Not found"}}}},"/v1/staff/{id}/documents":{"post":{"tags":["Staff"],"summary":"Upload a document for a staff member","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["document_type","file_url"],"properties":{"document_type":{"type":"string","enum":["aadhaar","pan","degree","experience","police_verification","photo","driving_licence","medical_fitness","other"]},"file_url":{"type":"string","format":"uri"},"expiry_date":{"type":"string","format":"date"}}}}}},"responses":{"200":{"description":"Document uploaded"}}},"get":{"tags":["Staff"],"summary":"List documents for a staff member","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Document list"}}}},"/v1/staff/{id}/documents/{docId}/verify":{"put":{"tags":["Staff"],"summary":"Verify a staff document","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"docId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["verified","rejected"]}}}}}},"responses":{"200":{"description":"Document verified"}}}},"/v1/staff/{id}/documents/{docId}":{"delete":{"tags":["Staff"],"summary":"Soft-delete a staff document","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"docId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Document deleted"}}}},"/v1/staff/{id}/onboard":{"post":{"tags":["Staff"],"summary":"Trigger onboarding checklist for a staff member","description":"Auto-generates employee ID step, login credentials placeholder, welcome SMS placeholder, and initializes payroll record.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Onboarding checklist created"}}}},"/v1/staff/{id}/onboarding":{"get":{"tags":["Staff"],"summary":"Get onboarding checklist status for a staff member","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Onboarding checklist"}}}},"/v1/staff/{id}/employment-type":{"put":{"tags":["Staff"],"summary":"Change staff employment type (logged)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["employment_type"],"properties":{"employment_type":{"type":"string","enum":["permanent","contract","visiting","substitute"]}}}}}},"responses":{"200":{"description":"Employment type updated"}}}},"/v1/staff/{id}/toggle-status":{"put":{"tags":["Staff"],"summary":"Toggle staff active / inactive status","description":"Flips a staff member between `active` and `inactive`. Returns 400 if the staff member has already exited.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Staff ID"}],"responses":{"200":{"description":"Status toggled successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Staff has exited and cannot be toggled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Staff not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/staff/{id}/account":{"post":{"tags":["Staff"],"summary":"Provision a login account for a staff member","description":"Creates a User account linked to the staff record, assigns the given role, and generates a temporary password. Credentials are NOT emailed automatically — use POST /staff/{id}/send-credentials to send login details. Returns 409 if the staff member already has an account.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Staff ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["role_id"],"properties":{"role_id":{"type":"string","description":"ID of the existing role to assign","example":"3"}}}}}},"responses":{"201":{"description":"Account provisioned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Staff has no email address on record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Staff or role not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"Staff already has a login account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/staff/{id}/send-credentials":{"post":{"tags":["Staff"],"summary":"Send (or resend) login credentials to a staff member","description":"Regenerates a temporary password and emails the staff member their login URL, email, and new temp password. Returns 409 if the staff has already logged in (resending would reset the password they set for themselves).","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Staff ID"}],"responses":{"200":{"description":"Login email enqueued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Staff has no email or no login account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Staff not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"Staff already activated their account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/staff/send-credentials":{"post":{"tags":["Staff"],"summary":"Bulk-send login credentials (e.g. after a staff import)","description":"Regenerates a temp password and emails login details to each staff member in the list. Staff who have already logged in, have no email, or have no account are skipped (never reset) and reported per-id. Safe to call for an entire import batch.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["staff_ids"],"properties":{"staff_ids":{"type":"array","items":{"type":"string"},"description":"Staff IDs to send credentials to (max 100)","example":["12","13","14"]}}}}}},"responses":{"200":{"description":"Per-staff send results with summary counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/search":{"get":{"tags":["Search"],"summary":"Global search across students, parents, staff, enquiries and fee slips","description":"Powers the admin top-bar global search. Runs 5 parallel queries\n(one per entity type) and returns up to 5 hits per group.\n\nScope rules:\n- Query is case-insensitive substring (`ILIKE`).\n- Results are scoped to the caller's branch when the JWT carries a\n  `branch_id`. School/platform admins (no branch on token) search\n  across all branches.\n- Each result group is gated by the corresponding view permission:\n  students/parents → `m3.student.view`, staff → `m5.staff.view`,\n  enquiries → `m4.enquiry.view`, invoices → `m9.fee_slip.view`.\n  Groups for which the caller lacks permission are returned as `[]`.\n- Queries shorter than 2 characters (after trim) return all groups\n  empty (HTTP 200).\n\nNote: `Enquiry.parent_phone` and `Staff.phone` are encrypted at rest\nand are not searchable from this endpoint.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"q","required":true,"schema":{"type":"string","minLength":1,"maxLength":100},"description":"Search term. Min 2 effective characters after trim; shorter inputs return empty groups."}],"responses":{"200":{"description":"Search results grouped by entity type.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiSuccessResponse"},{"type":"object","properties":{"data":{"type":"object","properties":{"students":{"type":"array","items":{"$ref":"#/components/schemas/SearchHit"}},"parents":{"type":"array","items":{"$ref":"#/components/schemas/SearchHit"}},"staff":{"type":"array","items":{"$ref":"#/components/schemas/SearchHit"}},"enquiries":{"type":"array","items":{"$ref":"#/components/schemas/SearchHit"}},"invoices":{"type":"array","items":{"$ref":"#/components/schemas/SearchHit"}}}}}}]}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/school-configs":{"get":{"tags":["SchoolConfig"],"summary":"Get school configuration values for a given group","description":"Returns the key/value map stored under the requested `group` in the\nschool_configs table. Used by settings UIs (e.g. timetable\nconfiguration) to pre-fill defaults captured during onboarding.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"group","required":true,"schema":{"type":"string","example":"schedule"},"description":"configGroup to filter by. Known groups: `schedule`, `fees`.\n"}],"responses":{"200":{"description":"Map of configKey to configValue (JSON values returned verbatim)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Missing or invalid `group` query parameter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["SchoolConfig"],"summary":"Save school configuration values for a group","description":"Upserts the school's configuration for the given group into the\nschool_configs table and returns the refreshed key/value map (same\nshape `GET /school-configs?group=<group>` returns). Only the\n`schedule` and `ib` groups are accepted; each key is persisted\nindependently. For `schedule`, `school_end_time` must be strictly\nafter `school_start_time`.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["group","values"],"properties":{"group":{"type":"string","enum":["schedule"],"example":"schedule"},"values":{"type":"object","required":["school_start_time","school_end_time","working_days","saturday_schedule"],"properties":{"school_start_time":{"type":"string","example":"08:00"},"school_end_time":{"type":"string","example":"16:00"},"working_days":{"type":"array","minItems":1,"items":{"type":"string","enum":["Mon","Tue","Wed","Thu","Fri","Sat"]},"example":["Mon","Tue","Wed","Thu","Fri","Sat"]},"saturday_schedule":{"type":"string","enum":["full","half","off"],"example":"half"},"saturday_start_time":{"type":"string","description":"Optional distinct Saturday start time; omit when Saturday inherits weekday hours. Must be sent together with saturday_end_time.","example":"08:00"},"saturday_end_time":{"type":"string","description":"Optional distinct Saturday end time; must be after saturday_start_time.","example":"12:00"}}}}},{"type":"object","required":["group","values"],"properties":{"group":{"type":"string","enum":["ib"],"example":"ib"},"values":{"type":"object","required":["ib_dp_enforce_subject_rules"],"properties":{"ib_dp_enforce_subject_rules":{"type":"boolean","description":"Whether IB DP subject-selection rules are enforced.","example":true}}}}}]}}}},"responses":{"200":{"description":"The refreshed key/value map for the group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Invalid body (missing field, bad saturday type, or end <= start)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/payroll/salary-templates":{"get":{"tags":["Salary Templates"],"summary":"List salary templates with their rules and assigned staff count","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":500}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"branch_id","schema":{"type":"string"}},{"in":"query","name":"employment_type","schema":{"type":"string","enum":["permanent","contract","visiting","substitute"]}},{"in":"query","name":"is_active","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Templates","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Salary Templates"],"summary":"Create a salary template","description":"A template holds rules, never rupee amounts. Exactly one component must be the balancing figure, and a basic component is required because percent-of-basic rules and PF both anchor on it.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","branch_id","components"],"properties":{"name":{"type":"string","example":"Teacher — Grade A"},"description":{"type":"string"},"employment_type":{"type":"string","enum":["permanent","contract","visiting","substitute"]},"structure_type":{"type":"string","enum":["flat","component"]},"branch_id":{"type":"string"},"is_active":{"type":"boolean"},"components":{"type":"array","items":{"type":"object","required":["component_id","rule_type"],"properties":{"component_id":{"type":"string"},"rule_type":{"type":"string","enum":["percent_of_gross","percent_of_basic","fixed","balance"]},"rule_value":{"type":"number"},"display_order":{"type":"integer"}}}}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/payroll/salary-templates/{id}":{"get":{"tags":["Salary Templates"],"summary":"Get one salary template with its component rules","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Template","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Salary Templates"],"summary":"Update a salary template","description":"Editing a template changes nobody's pay. Existing structures are untouched until an explicit, dated re-apply. Supplying `components` replaces the rule set wholesale.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string","nullable":true},"employment_type":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"components":{"type":"array","items":{"type":"object"}}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"delete":{"tags":["Salary Templates"],"summary":"Delete a salary template","description":"Refused while any staff member is on it. Deactivate instead.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"In use","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/payroll/salary-templates/{id}/staff":{"get":{"tags":["Salary Templates"],"summary":"List staff currently on a template","description":"The audience for a re-apply.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Staff","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/payroll/salary-templates/preview":{"post":{"tags":["Salary Templates"],"summary":"Derive a salary split from an annual CTC without saving","description":"Accepts a saved `template_id` or an inline `components` list, so an unsaved template can be previewed. Every component is returned with both a monthly and an annual figure. `employee_deductions` is read-only context from the existing deduction settings, which this endpoint does not modify.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["annual_ctc"],"properties":{"annual_ctc":{"type":"number","example":720000},"template_id":{"type":"string"},"components":{"type":"array","items":{"type":"object"}},"overrides":{"type":"object","description":"Component id → monthly amount. The balance row may not be overridden.","additionalProperties":{"type":"number"}}}}}}},"responses":{"200":{"description":"Derived split","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Template or CTC invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/payroll/salary-structures/from-template":{"post":{"tags":["Salary Templates"],"summary":"Assign a template to one staff member","description":"Creates a NEW dated structure. The current one is closed with an effective-to date, never edited, so approved payslips keep pointing at what was true when they were paid. Only overridden components are stored as overrides; the rest keep resolving from the template.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["staff_id","template_id","annual_ctc","effective_from","branch_id"],"properties":{"staff_id":{"type":"string"},"template_id":{"type":"string"},"annual_ctc":{"type":"number","example":720000},"effective_from":{"type":"string","example":"2026-04-01"},"branch_id":{"type":"string"},"overrides":{"type":"object","additionalProperties":{"type":"number"}}}}}}},"responses":{"201":{"description":"Structure created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Blocked by an approved payroll","or invalid input":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/payroll/salary-templates/{id}/bulk-preview":{"post":{"tags":["Salary Templates"],"summary":"Preview a re-apply across every staff member on a template","description":"Mutates nothing. Each row reports changed, unchanged or blocked, with the count of overrides that will be preserved.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["effective_from","branch_id"],"properties":{"effective_from":{"type":"string","example":"2026-04-01"},"branch_id":{"type":"string"},"staff_ids":{"type":"array","items":{"type":"string"}},"annual_ctc_by_staff":{"type":"object","additionalProperties":{"type":"number"}}}}}}},"responses":{"200":{"description":"Per-staff old vs new","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/payroll/salary-templates/{id}/bulk-apply":{"post":{"tags":["Salary Templates"],"summary":"Apply a template to many staff at once","description":"Creates one new dated structure per applied staff member. Blocked rows are skipped and returned in `skipped`, so a partial success is stated plainly rather than reported as a success.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["effective_from","branch_id"],"properties":{"effective_from":{"type":"string"},"branch_id":{"type":"string"},"staff_ids":{"type":"array","items":{"type":"string"}},"annual_ctc_by_staff":{"type":"object","additionalProperties":{"type":"number"}}}}}}},"responses":{"200":{"description":"Applied and skipped counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/salary-component-masters":{"get":{"tags":["Salary Component Masters"],"summary":"List salary components (paginated + searchable)","description":"The components a salary structure can be built from. Replaces the fixed SalaryComponentName enum, so a school can add its own allowances without a code change.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":500}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"is_active","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Salary components","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Salary Component Masters"],"summary":"Add a school-specific salary component","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code","name"],"properties":{"code":{"type":"string","example":"HOSTEL"},"name":{"type":"string","example":"Hostel Warden Allowance"},"description":{"type":"string"},"is_wage":{"type":"boolean","description":"Counts toward PF/ESI wages. Recorded","not yet applied.":null},"is_taxable":{"type":"boolean"},"display_order":{"type":"integer"},"is_active":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/salary-component-masters/{id}":{"get":{"tags":["Salary Component Masters"],"summary":"Get a salary component by ID","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Salary component","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Salary Component Masters"],"summary":"Update a salary component","description":"`code` cannot be changed — it is the stable identifier that salary structures reference. A system component may be renamed or reordered but never deactivated.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string","nullable":true},"is_wage":{"type":"boolean"},"is_taxable":{"type":"boolean"},"display_order":{"type":"integer"},"is_active":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"422":{"description":"Validation error or system component","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"delete":{"tags":["Salary Component Masters"],"summary":"Delete a salary component","description":"Refused for system components and for any component referenced by an existing salary structure. Deactivate (`is_active: false`) instead — that hides it from new structures while existing ones keep resolving.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"422":{"description":"System component or in use","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/role-templates":{"get":{"tags":["Role Templates"],"summary":"List all role templates (paginated, filterable)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":200}},{"in":"query","name":"scope_level","schema":{"type":"string"},"description":"Filter by scope_level (tenant, branch, class, section, own)"},{"in":"query","name":"is_platform_role","schema":{"type":"boolean"}},{"in":"query","name":"search","schema":{"type":"string"}}],"responses":{"200":{"description":"List of role templates","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"403":{"description":"Missing permission","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/role-templates/{id}":{"get":{"tags":["Role Templates"],"summary":"Get a single role template with permissions","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Role template with permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/roles/clone":{"post":{"tags":["Roles"],"summary":"Clone a role template into a custom role","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["template_id","name"],"properties":{"template_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"}}}}}},"responses":{"201":{"description":"Cloned role"},"404":{"description":"Template not found"}}}},"/v1/roles":{"get":{"tags":["Roles"],"summary":"List roles (filterable by is_custom, scope_level)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":200}},{"in":"query","name":"is_custom","schema":{"type":"boolean"}},{"in":"query","name":"scope_level","schema":{"type":"string"}},{"in":"query","name":"search","schema":{"type":"string"}}],"responses":{"200":{"description":"List of roles","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/roles/{id}":{"get":{"tags":["Roles"],"summary":"Get a single role with permissions","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Role with permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Roles"],"summary":"Update a custom role's name/description","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not a custom role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"delete":{"tags":["Roles"],"summary":"Soft-delete a custom role (only is_custom=true)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not a custom role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/roles/{id}/permissions":{"post":{"tags":["Roles"],"summary":"Add permissions to a custom role","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["permission_ids"],"properties":{"permission_ids":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Role with updated permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not a custom role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"delete":{"tags":["Roles"],"summary":"Remove permissions from a custom role","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["permission_ids"],"properties":{"permission_ids":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Role with updated permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not a custom role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/user-roles":{"post":{"tags":["User Roles"],"summary":"Assign a role to a user","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_id","role_id"],"properties":{"user_id":{"type":"string"},"role_id":{"type":"string"},"branch_id":{"type":"string"}}}}}},"responses":{"201":{"description":"Role assigned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"User or role not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"delete":{"tags":["User Roles"],"summary":"Revoke a role from a user","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_id","role_id"],"properties":{"user_id":{"type":"string"},"role_id":{"type":"string"}}}}}},"responses":{"200":{"description":"Role revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"User-role assignment not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/users/{id}/effective-permissions":{"get":{"tags":["User Roles"],"summary":"Get the union of all role permissions for a user (cached 30min)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Effective permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/roles/{id}/reset":{"post":{"tags":["Roles"],"summary":"Reset a role's permissions back to the original template defaults","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Role with restored default permissions"},"404":{"description":"Role or template not found"},"422":{"description":"Role has no source template"}}}},"/v1/promotions":{"get":{"tags":["Promotions"],"summary":"List promotion batches","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Promotion batch list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"post":{"tags":["Promotions"],"summary":"Create a promotion batch","description":"Collects all students in from_class for the academic year, creates draft promotion records","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["academic_year_id","from_class_id","to_class_id"],"properties":{"academic_year_id":{"type":"string"},"from_class_id":{"type":"string"},"to_class_id":{"type":"string"}}}}}},"responses":{"200":{"description":"Batch created"}}}},"/v1/promotions/{id}":{"get":{"tags":["Promotions"],"summary":"Get promotion batch detail with all records","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Promotion batch detail"},"404":{"description":"Not found"}}},"put":{"tags":["Promotions"],"summary":"Mark exceptions (detained, leaving, transferred) on a promotion batch","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["exceptions"],"properties":{"exceptions":{"type":"array","items":{"type":"object","required":["student_id","result"],"properties":{"student_id":{"type":"string"},"result":{"type":"string","enum":["promoted","detained","transferred","leaving"]},"to_section_id":{"type":"string"},"notes":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"Exceptions updated"},"400":{"description":"Batch not in draft status"},"404":{"description":"Batch or record not found"}}}},"/v1/promotions/{id}/execute":{"post":{"tags":["Promotions"],"summary":"Execute a promotion batch","description":"Creates new StudentAcademic records for promoted students, handles detained/transferred/leaving","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Promotion executed"},"400":{"description":"Batch not in draft status or next academic year not found"},"404":{"description":"Batch not found"}}}},"/api/platform/v1/demo-requests":{"get":{"tags":["Platform - Demo"],"summary":"List Try Demo leads (DemoRequest rows)","description":"Paginated list of everyone who submitted the public Try Demo email form.\nFilter by email search and whether the lead ever clicked through (`has_logged_in`).\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"search","required":false,"schema":{"type":"string"},"description":"Case-insensitive email contains filter"},{"in":"query","name":"has_logged_in","required":false,"schema":{"type":"boolean"},"description":"When true, only leads who explored the demo; when false, only never logged in"},{"in":"query","name":"page","required":false,"schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Demo request leads","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/platform/v1/enquiries/{enquiryId}":{"delete":{"tags":["Platform Enquiries"],"summary":"Soft-delete an un-converted enquiry","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"enquiryId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Enquiry is converted and cannot be deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Enquiry not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/api/platform/v1/schools/by-subdomain/{slug}":{"get":{"tags":["Platform - Schools"],"summary":"Public — fetch a school's name, status, and branding by URL subdomain.","description":"No authentication required. Used by the tenant frontend at page-load to\nconfirm the subdomain belongs to a real school, get the display name to\nrender on the login page, and detect suspended/churned schools.\n","parameters":[{"in":"path","name":"slug","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"School branding","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Invalid subdomain","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"403":{"description":"School suspended or churned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"School not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/pincode-masters":{"get":{"tags":["Pincode Masters"],"summary":"List pincodes (paginated, filterable)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Pincode Masters"],"summary":"Create a new pincode record","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created"}}}},"/v1/pincode-masters/lookup/{pincode}":{"get":{"tags":["Pincode Masters"],"summary":"Lookup a pincode (auto-fill city/state on form)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"pincode","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Pincode info or null","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/pincode-masters/{id}":{"get":{"tags":["Pincode Masters"],"summary":"Get a pincode record by ID","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Pincode","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"put":{"tags":["Pincode Masters"],"summary":"Update a pincode record (pincode value itself cannot change)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated"}}},"delete":{"tags":["Pincode Masters"],"summary":"Soft-delete a pincode record","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"}}}},"/v1/permissions":{"get":{"tags":["Permissions"],"summary":"List permissions (paginated + filterable)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1},"description":"1-based page index. Omit to return all records."},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":200},"description":"Page size. Defaults to 20 when pagination is requested."},{"in":"query","name":"module","schema":{"type":"string"},"description":"Filter by module name (exact match)."},{"in":"query","name":"resource","schema":{"type":"string"},"description":"Filter by resource name (exact match)."},{"in":"query","name":"action","schema":{"type":"string"},"description":"Filter by action type (exact match)."},{"in":"query","name":"is_deprecated","schema":{"type":"boolean"},"description":"Filter by deprecated status."},{"in":"query","name":"search","schema":{"type":"string"},"description":"Case-insensitive search across key, module, resource, action, description."}],"responses":{"200":{"description":"List of permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"403":{"description":"Missing permission","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/permissions/{id}":{"get":{"tags":["Permissions"],"summary":"Get a single permission by ID","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Permission","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/permissions/grouped":{"get":{"tags":["Permissions"],"summary":"Get all permissions grouped by module (for role builder UI)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Permissions grouped by module","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"403":{"description":"Missing permission","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/students/{id}/parent-documents":{"get":{"tags":["Parent Documents"],"summary":"Parent documents for a student","description":"Every parent on the student's family record with the documents held against them. Siblings return the same parent documents — a parent's papers are stored once against their user account, not per child.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Student id"}],"responses":{"200":{"description":"Parents and their documents","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Student family not found"}}}},"/v1/students/{id}/parents/{slot}/documents":{"post":{"tags":["Parent Documents"],"summary":"Upload a document for one of a student's parents","description":"Resolves the parent from the student's family record by phone number, so the upload is shared by every sibling of that parent. Staff-only. The parent's user account is created on first upload when it does not exist yet.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Student id"},{"in":"path","name":"slot","required":true,"schema":{"type":"string","enum":["father","mother","guardian"]}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file","document_type"],"properties":{"file":{"type":"string","format":"binary"},"document_type":{"type":"string","example":"aadhaar"}}}}}},"responses":{"201":{"description":"Document uploaded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Missing file","or the parent slot has no phone number":null},"404":{"description":"Student family not found"}}}},"/v1/parent-documents/{parentUserId}/documents":{"get":{"tags":["Parent Documents"],"summary":"List a parent's documents","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"parentUserId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Documents","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Parent not found"}}}},"/v1/parent-documents/{parentUserId}/documents/{docId}/verify":{"put":{"tags":["Parent Documents"],"summary":"Verify or reject a parent document","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"parentUserId","required":true,"schema":{"type":"string"}},{"in":"path","name":"docId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["verified","rejected"]},"remarks":{"type":"string"}}}}}},"responses":{"200":{"description":"Document updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Document not found"}}}},"/v1/parent-documents/{parentUserId}/documents/{docId}":{"delete":{"tags":["Parent Documents"],"summary":"Delete a parent document","description":"Soft-deletes the document. The file is shared by every child of this parent, so it is removed for all of them.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"parentUserId","required":true,"schema":{"type":"string"}},{"in":"path","name":"docId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Document deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Document not found"}}}},"/v1/parent/children":{"get":{"tags":["Parent"],"summary":"List the logged-in parent's own children (phone-matched)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Children","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/parent/children/{id}":{"get":{"tags":["Parent"],"summary":"Get one child's profile (parent-safe subset)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Child profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not your child","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/parent/children/{id}/attendance":{"get":{"tags":["Parent"],"summary":"Get one child's attendance","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"query","name":"academic_year_id","schema":{"type":"string"}},{"in":"query","name":"date","schema":{"type":"string","format":"date"}},{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}}],"responses":{"200":{"description":"Attendance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}},"403":{"description":"Not your child","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/parent/children/{id}/homework":{"get":{"tags":["Parent"],"summary":"Get homework for one child's current class/section","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"query","name":"academic_year_id","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}}],"responses":{"200":{"description":"Homework","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}},"403":{"description":"Not your child","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/parent/children/{id}/remarks":{"get":{"tags":["Parent"],"summary":"Teacher remarks shared for this child","description":"Only remarks a teacher or principal explicitly released to the parent. Internal staff notes and drafts are never returned.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}},{"in":"query","name":"category","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}},"403":{"description":"Not your child","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/parent/children/{id}/exam-schedule":{"get":{"tags":["Parent"],"summary":"Exam timetable for the parent's own child","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"query","name":"exam_config_id","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/ApiSuccessResponse"},"403":{"$ref":"#/components/responses/ApiErrorResponse"}}}},"/v1/parent/children/{id}/report-cards":{"get":{"tags":["Parent"],"summary":"Published report cards for the parent's own child","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"query","name":"exam_config_id","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/ApiSuccessResponse"},"403":{"$ref":"#/components/responses/ApiErrorResponse"}}}},"/v1/parent/children/{id}/remarks/{remarkId}":{"get":{"tags":["Parent"],"summary":"One shared remark","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"remarkId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found or not shared","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/parent/remarks/{remarkId}/acknowledge":{"post":{"tags":["Parent"],"summary":"Mark a shared remark as seen","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"remarkId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Acknowledged","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/parent/children/{id}/timetable":{"get":{"tags":["Parent"],"summary":"Get one child's finalized weekly timetable (with period slots)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Timetable (or null if none finalized)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not your child","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/parent/children/{id}/fees":{"get":{"tags":["Parent"],"summary":"Get fee slips for one child","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"query","name":"academic_year_id","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}}],"responses":{"200":{"description":"Fees","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}},"403":{"description":"Not your child","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/parent/children/{id}/fees/structure":{"get":{"tags":["Parent"],"summary":"Annual fee structure (total + heads) for one child's class","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Fee structure (or null)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not your child","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/parent/children/{id}/transport":{"get":{"tags":["Parent"],"summary":"Get one child's transport arrangement (route, stop, times, bus, crew)","description":"Static reference data only — contains no bus position, so it is safe to serve at any hour. Returns `data: null` when the child has no active route assignment (day scholar, or the assignment has ended). Driver and attendant phone numbers are returned so the app can offer a call button; each is null only when that staff record has no number on file. Includes the child's own stop coordinates and the school endpoint (`school_latitude`/`school_longitude`, same field names as the admin route API). Any coordinate is null when that record was never geocoded.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Transport arrangement","or null when the child does not use the bus":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not your child","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/parent/children/{id}/transport/live":{"get":{"tags":["Parent"],"summary":"Live bus position and progress, only while a trip is running","description":"Returns the bus position ONLY while a trip on the child's route is in_progress. Once the driver ends the trip — and at every other hour of the day — `is_live` is false and every position field is null, even though the completed trip still holds its final coordinates. A position whose last fix is older than PARENT_TRANSPORT_LOCATION_STALE_MINUTES is suppressed as well, with `location_is_stale: true` so the client can say \"location unavailable\" rather than render an empty map. Boarding status is scoped to this child alone — never the trip roster. Returns `data: null` when the child has no active route assignment. Each entry in `stops[]` carries `latitude`/`longitude` when the stop has been geocoded (null otherwise) so a client can plot stop markers, plus the school endpoint (`school_latitude`/`school_longitude`) the bus is heading to in the morning and departs from in the evening. The school is route reference data, so it is returned even when no trip is running. Note only the latest bus fix is stored — there is no travelled path to draw.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Live trip view","or null when the child does not use the bus":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not your child","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/parent/notices":{"get":{"tags":["Parent"],"summary":"List notices addressed to the parent (their children's class/section + school-wide)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}}],"responses":{"200":{"description":"Notices","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/parent/notices/{id}":{"get":{"tags":["Parent"],"summary":"Read one notice (marks it read); 403 if not addressed to the parent","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Notice","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not addressed to you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/parent/events":{"get":{"tags":["Parent"],"summary":"List approved events relevant to the parent","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"start_date","schema":{"type":"string","format":"date"}},{"in":"query","name":"end_date","schema":{"type":"string","format":"date"}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}}],"responses":{"200":{"description":"Events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/parent/gallery":{"get":{"tags":["Parent"],"summary":"List published photo albums shared with the parent","description":"Returns cover art and photo counts only — open one album to get its photos. Drafts and albums targeted at other classes are excluded.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"start_date","schema":{"type":"string","format":"date"}},{"in":"query","name":"end_date","schema":{"type":"string","format":"date"}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}}],"responses":{"200":{"description":"Albums","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/parent/gallery/{id}":{"get":{"tags":["Parent"],"summary":"Get one shared album with its photos","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Album with photos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Album not shared with this parent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/parent/holidays":{"get":{"tags":["Parent"],"summary":"List named holidays for the parent's children","description":"Branch-scoped holidays from the Holiday table. Optionally narrow to a single child via student_id, or to an academic year via academic_year_id.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"student_id","schema":{"type":"string"}},{"in":"query","name":"academic_year_id","schema":{"type":"string"}}],"responses":{"200":{"description":"Holidays","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/parent/queries":{"get":{"tags":["Parent"],"summary":"List the parent's own message threads","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Conversations","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"post":{"tags":["Parent"],"summary":"Send a query about a child — auto-routed to the class teacher. Requires text and/or a file.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["student_id"],"properties":{"student_id":{"type":"string"},"content":{"type":"string","description":"Optional when a file is attached"},"file":{"type":"string","format":"binary","description":"Optional document/image attachment"}}}}}},"responses":{"201":{"description":"Sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/parent/queries/{otherUserId}":{"get":{"tags":["Parent"],"summary":"Messages in the thread between the parent and one staff member (marks the thread's incoming messages read)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"otherUserId","required":true,"schema":{"type":"string"}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}}],"responses":{"200":{"description":"Messages","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/module-activations":{"get":{"tags":["Module Activations"],"summary":"List all modules with their activation status","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List of module activations","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"403":{"description":"Missing permission","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/module-activations/{moduleKey}":{"put":{"tags":["Module Activations"],"summary":"Toggle a module on or off","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"moduleKey","required":true,"schema":{"type":"string"},"description":"The module key (e.g. \"attendance\", \"transport\")"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["is_enabled"],"properties":{"is_enabled":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated module activation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Module key not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/lookup-masters":{"get":{"tags":["Lookup Masters"],"summary":"List lookup master entries (paginated + filtered)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"type","schema":{"type":"string"},"description":"Filter by lookup type (caste, religion, blood_group, etc.)"},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"is_active","schema":{"type":"boolean"}},{"in":"query","name":"parent_id","schema":{"type":"string"}},{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":500}}],"responses":{"200":{"description":"List of lookups","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"403":{"description":"Missing permission","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"post":{"tags":["Lookup Masters"],"summary":"Create a new lookup entry","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","code","name"],"properties":{"type":{"type":"string","example":"caste"},"code":{"type":"string","example":"GEN"},"name":{"type":"string","example":"General"},"parent_id":{"type":"string"},"display_order":{"type":"integer"},"is_active":{"type":"boolean"},"extra_data":{"type":"object"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/lookup-masters/types":{"get":{"tags":["Lookup Masters"],"summary":"List distinct lookup types in use","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Lookup type list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/lookup-masters/{id}":{"get":{"tags":["Lookup Masters"],"summary":"Get a single lookup entry by ID","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Lookup entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Lookup Masters"],"summary":"Update a lookup entry (cannot change type/code/system flag)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"parent_id":{"type":"string","nullable":true},"display_order":{"type":"integer"},"is_active":{"type":"boolean"},"extra_data":{"type":"object"}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"delete":{"tags":["Lookup Masters"],"summary":"Soft-delete a lookup entry (blocks system rows)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"System row protected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/institution-masters":{"get":{"tags":["Institution Masters"],"summary":"List institutions (paginated, filterable by state_id, type)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Institution Masters"],"summary":"Create a new institution","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created"}}}},"/v1/institution-masters/{id}":{"get":{"tags":["Institution Masters"],"summary":"Get an institution by ID","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Institution","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"put":{"tags":["Institution Masters"],"summary":"Update an institution","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated"}}},"delete":{"tags":["Institution Masters"],"summary":"Soft-delete an institution","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"}}}},"/v1/imports/upload":{"post":{"tags":["Imports"],"summary":"Upload an Excel file and create an import job","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["entity_type","file_url"],"properties":{"entity_type":{"type":"string"},"file_url":{"type":"string","format":"uri"}}}}}},"responses":{"201":{"description":"Import job created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/imports/{id}/map-columns":{"post":{"tags":["Imports"],"summary":"Save column mapping for an import job","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["mapping"],"properties":{"mapping":{"type":"object","additionalProperties":{"type":"string"}}}}}}},"responses":{"200":{"description":"Mapping saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Import job not found"}}}},"/v1/imports/{id}/validate":{"post":{"tags":["Imports"],"summary":"Run validation on an import job","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Validation result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Import job not found"}}}},"/v1/imports/{id}/execute":{"post":{"tags":["Imports"],"summary":"Execute the import (create records)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Import executed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Import job not found"}}}},"/v1/imports":{"get":{"tags":["Imports"],"summary":"List import jobs","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"entity_type","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string","enum":["uploaded","mapping","validating","validated","importing","completed","failed"]}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}}],"responses":{"200":{"description":"List of import jobs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/imports/{id}":{"get":{"tags":["Imports"],"summary":"Get a single import job with status and error details","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Import job detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found"}}}},"/v1/id-cards/config":{"get":{"tags":["ID Cards"],"summary":"Get the ID card template config for the authenticated user's branch","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The branch ID card config, or product defaults when unset","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Unauthorized"}}},"put":{"tags":["ID Cards"],"summary":"Save the ID card template config (preset, orientation, branding)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated config"},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"}}}},"/v1/id-cards/presets":{"get":{"tags":["ID Cards"],"summary":"List the available ID card presets","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Preset metadata (id","name":null,"description)":null},"401":{"description":"Unauthorized"}}}},"/v1/id-cards/config/signature":{"post":{"tags":["ID Cards"],"summary":"Upload the principal signature printed on every card","security":[{"bearerAuth":[]}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"signature":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"Updated config"},"400":{"description":"Missing file or file too large"}}},"delete":{"tags":["ID Cards"],"summary":"Remove the principal signature","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated config"}}}},"/v1/id-cards/preview":{"get":{"tags":["ID Cards"],"summary":"Render the current config against sample data, as a PDF","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"A one-card PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Unauthorized"}}}},"/v1/id-cards/student/{id}":{"post":{"tags":["ID Cards"],"summary":"Generate an ID card for one student","description":"Creates the card record immediately and renders the PDF in the background, so poll the record until pdf_url is populated.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The created card record"},"401":{"description":"Unauthorized"},"404":{"description":"Student not found"}}},"get":{"tags":["ID Cards"],"summary":"List every ID card issued to a student","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Card records","newest first":null},"401":{"description":"Unauthorized"}}}},"/v1/id-cards/{cardId}/download":{"get":{"tags":["ID Cards"],"summary":"Get the PDF URL for a generated card","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"cardId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"pdf_url and file_name"},"404":{"description":"Card not found"},"409":{"description":"PDF not ready yet"}}}},"/v1/id-cards/preflight":{"post":{"tags":["ID Cards"],"summary":"Flag students who would print a defective card","description":"Reports missing photos and GR numbers before anything renders.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Counts plus a per-student issue list"},"400":{"description":"Validation error"}}}},"/v1/id-cards/batch":{"post":{"tags":["ID Cards"],"summary":"Create a bulk ID card run and preflight it","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The batch","status \"ready\"":null,"with preflight findings":null},"400":{"description":"Validation error"}}},"get":{"tags":["ID Cards"],"summary":"List recent bulk runs for the branch","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Batches","newest first":null}}}},"/v1/id-cards/batch/{batchId}":{"get":{"tags":["ID Cards"],"summary":"Poll one bulk run","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"batchId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The batch","including progress counts":null},"404":{"description":"Batch not found"}}}},"/v1/id-cards/batch/{batchId}/generate":{"post":{"tags":["ID Cards"],"summary":"Start rendering a bulk run","description":"Returns immediately; rendering continues in the background. Poll the batch until status is completed or failed.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"batchId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The batch","status \"generating\"":null},"409":{"description":"Batch is not in a startable state"}}}},"/v1/id-cards/batch/{batchId}/download":{"get":{"tags":["ID Cards"],"summary":"Get the merged PDF for a finished run","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"batchId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"pdf_url and file_name"},"409":{"description":"PDF not ready yet"}}}},"/v1/homework":{"get":{"tags":["Homework"],"summary":"List homework","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"class_id","schema":{"type":"string"}},{"in":"query","name":"section_id","schema":{"type":"string"}},{"in":"query","name":"subject_id","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string","enum":["active","cancelled"]}}],"responses":{"200":{"description":"Homework list"}}},"post":{"tags":["Homework"],"summary":"Create homework","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["class_id","section_id","subject_id","title","description","due_date","academic_year_id"]}}}},"responses":{"200":{"description":"Created homework"}}}},"/v1/homework/{id}":{"get":{"tags":["Homework"],"summary":"Get homework by ID","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Homework detail"}}},"put":{"tags":["Homework"],"summary":"Update homework","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated homework"}}},"delete":{"tags":["Homework"],"summary":"Delete homework","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Homework deleted"}}}},"/v1/homework/{id}/upload-attachment":{"post":{"tags":["Homework"],"summary":"Upload a file attachment for a homework record","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"Returns updated homework with attachment_url"}}}},"/v1/syllabus-progress":{"get":{"tags":["Homework"],"summary":"List syllabus progress","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"academic_year_id","schema":{"type":"string"}},{"in":"query","name":"class_id","schema":{"type":"string"}}],"responses":{"200":{"description":"Syllabus progress list"}}}},"/v1/syllabus-progress/alerts":{"get":{"tags":["Homework"],"summary":"Get syllabus progress alerts (< 70% completion at 80% of term)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"academic_year_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Syllabus alerts"}}}},"/v1/gallery/albums":{"get":{"tags":["Gallery"],"summary":"List photo albums (cover art only — photos are fetched per album)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["draft","pending_approval","published"]}},{"in":"query","name":"branch_id","schema":{"type":"string"}},{"in":"query","name":"academic_year_id","schema":{"type":"string"}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"start_date","schema":{"type":"string","format":"date"}},{"in":"query","name":"end_date","schema":{"type":"string","format":"date"}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}}],"responses":{"200":{"description":"Album list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Gallery"],"summary":"Create a photo album (starts as draft — publish separately)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","target_audience","branch_id","academic_year_id"],"properties":{"title":{"type":"string"},"description":{"type":"string"},"event_date":{"type":"string","format":"date"},"target_audience":{"type":"object"},"branch_id":{"type":"string"},"academic_year_id":{"type":"string"}}}}}},"responses":{"201":{"description":"Album created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/gallery/albums/{id}":{"get":{"tags":["Gallery"],"summary":"Get one album with its photos","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Album with photos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Album not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Gallery"],"summary":"Update album details","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string","nullable":true},"event_date":{"type":"string","format":"date","nullable":true},"target_audience":{"type":"object"}}}}}},"responses":{"200":{"description":"Album updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"delete":{"tags":["Gallery"],"summary":"Delete an album and its photos","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Album deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/gallery/albums/{id}/publish":{"put":{"tags":["Gallery"],"summary":"Publish an album to parents (requires gallery.approve)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Album published","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Album has no photos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/gallery/albums/{id}/photos":{"post":{"tags":["Gallery"],"summary":"Upload photos into an album (max 20 images per request)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["photos"],"properties":{"photos":{"type":"array","items":{"type":"string","format":"binary"}},"captions":{"type":"string","description":"JSON array of captions, positionally matching photos"}}}}}},"responses":{"201":{"description":"Photos uploaded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/gallery/photos/{id}":{"delete":{"tags":["Gallery"],"summary":"Delete one photo from an album","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Photo deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/fees/structures/bulk-submit":{"put":{"tags":["Fee Structures"],"summary":"Submit multiple draft fee structures for approval (all-or-nothing)","description":"Transitions every supplied draft fee structure from `draft` to `pending_approval` in a single transaction. If any id is missing or not currently a draft, the whole request is rejected and nothing is submitted.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","minItems":1,"items":{"type":"string"},"description":"Fee structure ids to submit for approval."}}}}}},"responses":{"200":{"description":"Structures submitted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"One or more structures are not drafts or do not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/fees/invoices/next-installment":{"get":{"tags":["Fee Slips"],"summary":"Preview the next installment per class before generating fee slips","description":"Read-only companion to the bulk-generate endpoint, used by the\nGenerate Fee Slips dialog. For each class it reports the installment\nnumber the next generation run would produce, the total installments\nin the approved fee structure, and how many active students would\nreceive a fee slip (students already holding one for that installment\nare excluded). Classes with every installment already generated come\nback with `all_generated: true`; classes without an approved fee\nstructure come back with `no_structure: true`. Consumes no sequence\nnumbers and writes nothing.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"academic_year_id","required":true,"schema":{"type":"string"},"description":"Academic year ID (BigInt as string)"},{"in":"query","name":"class_ids","required":true,"schema":{"type":"string"},"description":"Comma-separated class IDs (BigInt as string), e.g. `1,2,3`"},{"in":"query","name":"branch_id","required":true,"schema":{"type":"string"},"description":"Branch ID (BigInt as string) — same semantics as bulk-generate"},{"in":"query","name":"section_id","required":false,"schema":{"type":"string"},"description":"Section ID (BigInt as string). Narrows eligible_students to one section. Only supported when class_ids contains a single class — same rule as bulk-generate (422 otherwise). next_installment and total_installments stay class-level.\n"}],"responses":{"200":{"description":"Per-class next-installment preview (`data.classes[]`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"422":{"description":"Invalid query params","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/fees/invoices":{"get":{"tags":["Fee Slips"],"summary":"List fee slips (filterable, searchable, optionally paginated)","description":"Returns fee slip rows for the Fee Slips table. Each row carries the\nserialized invoice plus `class_name`/`section_name` and\n`total_installments` — the installment cap of the class's approved\nfee structure (max of the structure items' installment counts, >= 1)\nso the UI can render \"installment 4/4\". `total_installments` is null\nwhen no approved structure resolves for the slip's class/year/branch.\n`status=overdue` matches past-due unpaid slips, not only rows whose\nstored status is literally \"overdue\".\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"student_id","required":false,"schema":{"type":"string"},"description":"Student ID (BigInt as string)"},{"in":"query","name":"academic_year_id","required":false,"schema":{"type":"string"},"description":"Academic year ID (BigInt as string)"},{"in":"query","name":"class_id","required":false,"schema":{"type":"string"},"description":"Class ID (BigInt as string)"},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["generated","partially_paid","paid","overdue","cancelled"]}},{"in":"query","name":"search","required":false,"schema":{"type":"string"},"description":"Case-insensitive search on student name, admission number, GR number"},{"in":"query","name":"branch_id","required":false,"schema":{"type":"string"},"description":"Branch ID (BigInt as string); defaults from the caller's scope"},{"in":"query","name":"page","required":false,"schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","required":false,"schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Fee slips (with optional pagination)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/fees/payments/bulk":{"post":{"tags":["Fees"],"summary":"Record one collected amount spread across multiple fee slips","description":"Creates one payment row per item. Accepts application/json, or multipart/form-data with an optional proof_file — the file is uploaded once and the same proof is attached to every payment created by this transaction (one collected amount = one proof).\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["student_id","items","payment_mode","payment_date","branch_id"],"properties":{"student_id":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"invoice_id":{"type":"string"},"amount":{"type":"number"}}}},"payment_mode":{"type":"string","enum":["cash","cheque","dd","upi","neft"]},"reference_number":{"type":"string"},"payment_date":{"type":"string","format":"date"},"branch_id":{"type":"string"},"apply_credit":{"type":"boolean"}}}},"multipart/form-data":{"schema":{"type":"object","properties":{"student_id":{"type":"string"},"items":{"type":"string","description":"JSON-encoded array of { invoice_id, amount }"},"payment_mode":{"type":"string","enum":["cash","cheque","dd","upi","neft"]},"reference_number":{"type":"string"},"payment_date":{"type":"string","format":"date"},"branch_id":{"type":"string"},"apply_credit":{"type":"string","enum":["true","false"]},"proof_file":{"type":"string","format":"binary"}}}}}},"responses":{"201":{"description":"Payments created (one per slip)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/fees/students/{studentId}/deposits":{"get":{"tags":["Fee Deposits"],"summary":"Refundable-deposit balance and ledger for a student","description":"Returns the derived deposit position (held − adjusted − refunded), broken down per refundable fee head, plus the full append-only ledger. `available` is what can still be adjusted against dues or refunded.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"studentId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deposit balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/fees/deposits/adjust":{"post":{"tags":["Fee Deposits"],"summary":"Apply held deposit against an outstanding invoice","description":"Settles part or all of an invoice from the student's held deposit. Records a `deposit_adjustment` payment so invoice rollups, payment reports and receipt history behave exactly as they do for cash, and writes an `adjusted` entry on the deposit ledger. Rejected when the amount exceeds either the available deposit or the invoice balance.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["student_id","invoice_id","amount"],"properties":{"student_id":{"type":"string"},"invoice_id":{"type":"string"},"amount":{"type":"number"},"fee_head_id":{"type":"string","description":"Refundable head to draw from"},"note":{"type":"string"}}}}}},"responses":{"200":{"description":"Adjustment applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/fees/deposits/refund":{"post":{"tags":["Fee Deposits"],"summary":"Raise a refund drawn from the held deposit","description":"Creates a pending refund against the student's deposit balance. It goes through the same approval chain as every other refund — the `refunded` ledger entry is written only on approval, so a pending request never overstates what has left the balance.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["student_id","amount","reason"],"properties":{"student_id":{"type":"string"},"amount":{"type":"number"},"reason":{"type":"string"},"fee_head_id":{"type":"string"}}}}}},"responses":{"201":{"description":"Refund requested","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/fees/receipts/backfill-pdfs":{"post":{"tags":["Fees"],"summary":"Generate PDFs for receipts that don't have one yet","description":"One-off maintenance — renders and stores the receipt PDF for every receipt whose pdf_url is null (created before receipt-PDF generation existed). Skips void receipts.","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"limit","schema":{"type":"integer","default":500},"description":"Max receipts to process in this run."}],"responses":{"200":{"description":"Backfill run summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/exams/marks/roster":{"get":{"tags":["Exams"],"summary":"Students a teacher may enter marks for (class + section + subject)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"class_id","required":true,"schema":{"type":"string"}},{"in":"query","name":"section_id","schema":{"type":"string"}},{"in":"query","name":"subject_id","required":true,"schema":{"type":"string"}},{"in":"query","name":"academic_year_id","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/ApiSuccessResponse"},"403":{"$ref":"#/components/responses/ApiErrorResponse"}}}},"/v1/exams/submission-summary":{"get":{"tags":["Exams"],"summary":"Per-section, per-subject marks submission progress","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"exam_config_id","required":true,"schema":{"type":"string"}},{"in":"query","name":"class_id","required":true,"schema":{"type":"string"}},{"in":"query","name":"academic_year_id","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/ApiSuccessResponse"}}}},"/v1/exams/report-cards":{"delete":{"tags":["Exams"],"summary":"Delete draft report cards (published ones are skipped)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"$ref":"#/components/responses/ApiSuccessResponse"},"404":{"$ref":"#/components/responses/ApiErrorResponse"}}}},"/v1/academic-calendars":{"get":{"tags":["Events & Calendar"],"summary":"List academic calendars","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Calendar list"}}}},"/v1/holidays":{"get":{"tags":["Events & Calendar"],"summary":"List holidays","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"calendar_id","schema":{"type":"string"}},{"in":"query","name":"holiday_type","schema":{"type":"string","enum":["public","school","optional"]}}],"responses":{"200":{"description":"Holiday list"}}}},"/v1/events":{"get":{"tags":["Events & Calendar"],"summary":"List events","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["draft","approved","cancelled"]}},{"in":"query","name":"branch_id","schema":{"type":"string"}}],"responses":{"200":{"description":"Event list"}}}},"/v1/ptm-sessions":{"get":{"tags":["Events & Calendar"],"summary":"List PTM sessions","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"PTM session list"}}}},"/v1/document-type-masters":{"get":{"tags":["Document Type Masters"],"summary":"List document types","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"applies_to","schema":{"type":"string","enum":["student","staff","parent","both"]}},{"in":"query","name":"is_required","schema":{"type":"boolean"}},{"in":"query","name":"search","schema":{"type":"string"}}],"responses":{"200":{"description":"List","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Document Type Masters"],"summary":"Create a new document type","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code","name","applies_to","allowed_mime_types"],"properties":{"code":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"applies_to":{"type":"string","enum":["student","staff","parent","both"]},"is_required":{"type":"boolean"},"max_size_mb":{"type":"integer"},"allowed_mime_types":{"type":"array","items":{"type":"string"}},"expiry_required":{"type":"boolean"},"display_order":{"type":"integer"},"is_active":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Created"}}}},"/v1/document-type-masters/{id}":{"get":{"tags":["Document Type Masters"],"summary":"Get a document type by ID","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Document type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"put":{"tags":["Document Type Masters"],"summary":"Update a document type","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated"}}},"delete":{"tags":["Document Type Masters"],"summary":"Soft-delete a document type","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"422":{"description":"In use / system row"}}}},"/v1/designation-masters":{"get":{"tags":["Designation Masters"],"summary":"List designations (paginated, filterable by department_id + is_teaching)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"department_id","schema":{"type":"string"}},{"in":"query","name":"is_teaching","schema":{"type":"boolean"}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"is_active","schema":{"type":"boolean"}},{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":200}}],"responses":{"200":{"description":"List of designations","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Designation Masters"],"summary":"Create a new designation","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code","name"],"properties":{"code":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"department_id":{"type":"string"},"is_teaching":{"type":"boolean"},"salary_grade":{"type":"string"},"display_order":{"type":"integer"},"is_active":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/designation-masters/{id}":{"get":{"tags":["Designation Masters"],"summary":"Get a designation by ID","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Designation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"put":{"tags":["Designation Masters"],"summary":"Update a designation","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"delete":{"tags":["Designation Masters"],"summary":"Soft-delete a designation","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"422":{"description":"In use / system row"}}}},"/v1/department-masters":{"get":{"tags":["Department Masters"],"summary":"List departments (paginated + searchable)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":200}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"is_active","schema":{"type":"boolean"}}],"responses":{"200":{"description":"List of departments","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Department Masters"],"summary":"Create a new department","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code","name"],"properties":{"code":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"cost_center":{"type":"string"},"head_staff_id":{"type":"string"},"display_order":{"type":"integer"},"is_active":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/department-masters/{id}":{"get":{"tags":["Department Masters"],"summary":"Get a department by ID","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Department","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Department Masters"],"summary":"Update a department","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string","nullable":true},"cost_center":{"type":"string","nullable":true},"head_staff_id":{"type":"string","nullable":true},"display_order":{"type":"integer"},"is_active":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"delete":{"tags":["Department Masters"],"summary":"Soft-delete a department (blocks if referenced)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"422":{"description":"In use / system row"}}}},"/v1/demo/request":{"post":{"tags":["Try Demo"],"summary":"Request a demo magic link by email (public, rate-limited)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"Link sent (or would be","if the email is valid)":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/demo/login":{"get":{"tags":["Try Demo"],"summary":"Auto-login via a demo magic link (public) — redirects to the demo tenant","parameters":[{"in":"query","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect to the demo tenant with a session attached"},"400":{"description":"Invalid or unknown token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/dashboards/owner":{"get":{"summary":"Owner dashboard aggregates","description":"School-wide KPIs for the owner dashboard — headcount, attendance, fee\ncollection, admissions, compliance, DAU, trend series, prior-period\ncounterparts, and monthly payroll cost. Cached for 2 minutes.\n\n**Payroll fields** are sourced from the most recent *generated* payroll\nrun for the branch (any status other than `draft`). If that run is for\nan earlier month than today, its figures are still returned with\n`payroll_is_fallback: true` and `payroll_period_label` naming the month\nthey belong to. If no generated run exists, all nine payroll fields are\n`null` (never `0` — a zero would read as a real zero-cost month).\n","tags":["Dashboards"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"branch_id","required":false,"schema":{"type":"string"},"description":"Scope every aggregate to one branch (BigInt as string). Omit for all branches."},{"in":"query","name":"attendance_month","required":false,"schema":{"type":"string","pattern":"^\\\\d{4}-(0[1-9]|1[0-2])$"},"example":"2026-06","description":"Calendar month (`YYYY-MM`) for the daily/weekly student attendance\nseries. A valid value uses that full month. Omit (or pass an invalid\nvalue) to keep the current month through today. Other owner KPIs are\nnot filtered by this parameter.\n"}],"responses":{"200":{"description":"Owner dashboard payload","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiSuccessResponse"},{"type":"object","properties":{"data":{"type":"object","properties":{"total_students":{"type":"integer"},"boys_count":{"type":"integer"},"girls_count":{"type":"integer"},"total_staff":{"type":"integer"},"total_teachers":{"type":"integer"},"other_staff":{"type":"integer"},"total_branches":{"type":"integer"},"attendance_academic_year_pct":{"type":"number"},"fee_collection_pct":{"type":"number"},"fee_outstanding":{"type":"number"},"fee_collected_this_month":{"type":"number"},"admissions_this_month":{"type":"integer"},"compliance_score":{"type":"integer"},"teacher_dau":{"type":"integer"},"parent_dau":{"type":"integer"},"payroll_cost_this_month":{"type":"number","nullable":true,"description":"Total cost to school = gross earned + employer PF + employer ESI."},"payroll_gross":{"type":"number","nullable":true,"description":"SUM(gross_earned) across the run's payslips."},"payroll_employer_pf":{"type":"number","nullable":true,"description":"SUM(employer_pf) — employer's PF contribution."},"payroll_employer_esi":{"type":"number","nullable":true,"description":"SUM(employer_esi) — employer's ESI contribution."},"payroll_net_paid":{"type":"number","nullable":true,"description":"SUM(net_pay) — take-home actually paid to staff."},"payroll_staff_count":{"type":"integer","nullable":true,"description":"Number of payslips in the run."},"payroll_period_label":{"type":"string","nullable":true,"example":"August 2026","description":"The month the payroll figures are actually for."},"payroll_is_fallback":{"type":"boolean","nullable":true,"description":"True when the figures are NOT for the current calendar month."},"prev_payroll_cost":{"type":"number","nullable":true,"description":"Same total-cost formula for the month before payroll_period_label; null when that month has no generated run."}}}}}]}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"403":{"description":"Missing permission","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/custom-fields":{"get":{"tags":["Custom Fields"],"summary":"List custom field definitions","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"entity_type","schema":{"type":"string","enum":["student","staff","admission","fee","transport"]},"required":false}],"responses":{"200":{"description":"List of definitions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Unauthenticated"},"403":{"description":"Missing permission"}}},"post":{"tags":["Custom Fields"],"summary":"Create a custom field definition","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["entity_type","field_name","field_type","is_required","display_order"],"properties":{"entity_type":{"type":"string","enum":["student","staff","admission","fee","transport"]},"field_name":{"type":"string"},"field_type":{"type":"string","enum":["text","number","date","dropdown","file"]},"dropdown_options":{"type":"object"},"is_required":{"type":"boolean"},"display_order":{"type":"integer"}}}}}},"responses":{"201":{"description":"Created definition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/custom-fields/{id}":{"put":{"tags":["Custom Fields"],"summary":"Update a custom field definition","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"field_name":{"type":"string"},"field_type":{"type":"string","enum":["text","number","date","dropdown","file"]},"dropdown_options":{"type":"object"},"is_required":{"type":"boolean"},"display_order":{"type":"integer"},"is_active":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated definition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found"}}},"delete":{"tags":["Custom Fields"],"summary":"Soft-delete a custom field definition","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found"}}}},"/v1/custom-fields/reorder":{"put":{"tags":["Custom Fields"],"summary":"Reorder custom field definitions (bulk display_order update)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fields"],"properties":{"fields":{"type":"array","items":{"type":"object","required":["id","display_order"],"properties":{"id":{"type":"string"},"display_order":{"type":"integer"}}}}}}}}},"responses":{"200":{"description":"Reordered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/custom-fields/{entityType}/{entityId}/values":{"get":{"tags":["Custom Fields"],"summary":"Get custom field values for an entity","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"entityType","required":true,"schema":{"type":"string","enum":["student","staff","admission","fee","transport"]}},{"in":"path","name":"entityId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Field values","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"put":{"tags":["Custom Fields"],"summary":"Bulk upsert custom field values for an entity","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"entityType","required":true,"schema":{"type":"string","enum":["student","staff","admission","fee","transport"]}},{"in":"path","name":"entityId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["values"],"properties":{"values":{"type":"array","items":{"type":"object","required":["definition_id"],"properties":{"definition_id":{"type":"string"},"value_text":{"type":"string","nullable":true},"value_number":{"type":"number","nullable":true},"value_date":{"type":"string","nullable":true},"value_json":{"type":"object"}}}}}}}}},"responses":{"200":{"description":"Upserted values","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/certificate-templates":{"get":{"tags":["Certificate Templates"],"summary":"List certificate templates","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"template_type","schema":{"type":"string","enum":["tc","bonafide","character","study","custom"]},"required":false}],"responses":{"200":{"description":"List of templates","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"post":{"tags":["Certificate Templates"],"summary":"Create a certificate template","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["template_type","name","placeholders"],"properties":{"template_type":{"type":"string","enum":["tc","bonafide","character","study","custom"]},"name":{"type":"string"},"file_url":{"type":"string","format":"uri","nullable":true},"placeholders":{"type":"object"},"is_default":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Created template","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/certificate-templates/{id}":{"get":{"tags":["Certificate Templates"],"summary":"Get a single certificate template","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Template detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found"}}},"put":{"tags":["Certificate Templates"],"summary":"Update a certificate template","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"file_url":{"type":"string","format":"uri","nullable":true},"placeholders":{"type":"object"},"is_default":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated template","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found"}}},"delete":{"tags":["Certificate Templates"],"summary":"Soft-delete a certificate template","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found"}}}},"/v1/certificate-templates/placeholders":{"get":{"tags":["Certificate Templates"],"summary":"List available placeholders by template type","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"template_type","schema":{"type":"string","enum":["tc","bonafide","character","study","custom"]},"required":false}],"responses":{"200":{"description":"Placeholder map","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/certificate-configs":{"get":{"tags":["Certificate Configs"],"summary":"List all certificate configs for the authenticated user's branch","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List of certificate configs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Unauthorized"}}}},"/v1/certificate-configs/{type}":{"get":{"tags":["Certificate Configs"],"summary":"Get the certificate config for a specific cert type","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"type","required":true,"schema":{"type":"string","enum":["tc","bonafide","character","study"]}}],"responses":{"200":{"description":"Certificate config detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Invalid cert type"},"401":{"description":"Unauthorized"},"404":{"description":"Config not found"}}},"put":{"tags":["Certificate Configs"],"summary":"Save builder HTML content for a cert type","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"type","required":true,"schema":{"type":"string","enum":["tc","bonafide","character","study"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["html_content"],"properties":{"html_content":{"type":"string"},"is_enabled":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated certificate config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"404":{"description":"Config not found"}}}},"/v1/certificate-configs/{type}/upload":{"post":{"tags":["Certificate Configs"],"summary":"Upload a .docx file for a cert type","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"type","required":true,"schema":{"type":"string","enum":["tc","bonafide","character","study"]}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"Updated certificate config with uploaded file URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Invalid file or missing placeholder"},"401":{"description":"Unauthorized"},"404":{"description":"Config not found"}}}},"/v1/certificate-configs/{type}/file":{"delete":{"tags":["Certificate Configs"],"summary":"Remove the uploaded .docx file for a cert type (reverts to builder mode)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"type","required":true,"schema":{"type":"string","enum":["tc","bonafide","character","study"]}}],"responses":{"200":{"description":"File removed, config reverted to builder mode","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Invalid cert type"},"401":{"description":"Unauthorized"},"404":{"description":"Config not found"}}}},"/v1/students/{id}/certificates/tc":{"post":{"tags":["Certificates"],"summary":"Generate a Transfer Certificate for a student","description":"Checks fee clearance, generates TC, and sets student status to transferred","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason","board_format"],"properties":{"reason":{"type":"string"},"board_format":{"type":"string"},"tc_data":{"type":"object"}}}}}},"responses":{"200":{"description":"TC generated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Fee clearance required"},"404":{"description":"Student not found"}}}},"/v1/students/{id}/certificates/{type}":{"post":{"tags":["Certificates"],"summary":"Generate a certificate (bonafide, character, study, custom)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"type","required":true,"schema":{"type":"string","enum":["bonafide","character","study","custom"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["template_id"],"properties":{"template_id":{"type":"string"}}}}}},"responses":{"200":{"description":"Certificate generated"},"404":{"description":"Student or template not found"}}}},"/v1/students/{id}/certificates":{"get":{"tags":["Certificates"],"summary":"List all certificates (TCs and other) for a student","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Certificate list"},"404":{"description":"Student not found"}}}},"/v1/students/{id}/certificates/{certId}/download":{"get":{"tags":["Certificates"],"summary":"Get PDF download URL for a certificate","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"certId","required":true,"schema":{"type":"string"}},{"in":"query","name":"tc","schema":{"type":"string","enum":["true","false"]},"description":"Pass tc=true for transfer certificates"}],"responses":{"200":{"description":"PDF URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"PDF not ready yet"},"404":{"description":"Certificate not found"}}}},"/v1/branding":{"get":{"tags":["Branding"],"summary":"Get current branding settings","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"branch_id","schema":{"type":"string"},"description":"Optional branch ID for branch-specific branding. Omit for school-wide."}],"responses":{"200":{"description":"Branding settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Branding"],"summary":"Update branding settings","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"branch_id","schema":{"type":"string"},"description":"Optional branch ID. Omit for school-wide."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"school_name":{"type":"string"},"primary_color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"secondary_color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"portal_title":{"type":"string"},"app_title":{"type":"string"},"poster_urls":{"type":"array","items":{"type":"string"},"description":"Full replacement of the poster list — send the remaining items to remove one."}}}}}},"responses":{"200":{"description":"Updated branding","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/branding/logo":{"post":{"tags":["Branding"],"summary":"Upload school logo","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"branch_id","schema":{"type":"string"},"description":"Optional branch ID. Omit for school-wide."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"logo":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"Updated branding with logo","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/branding/posters":{"post":{"tags":["Branding"],"summary":"Upload school poster images (appends to poster_urls)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"branch_id","schema":{"type":"string"},"description":"Optional branch ID. Omit for school-wide."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"posters":{"type":"array","items":{"type":"string","format":"binary"}}}}}}},"responses":{"200":{"description":"Updated branding with posters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/branch-transfers":{"post":{"tags":["Branch Transfers"],"summary":"Initiate a cross-branch transfer","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["entity_type","entity_id","to_branch_id"],"properties":{"entity_type":{"type":"string","enum":["student","staff"]},"entity_id":{"type":"string"},"to_branch_id":{"type":"string"},"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Transfer initiated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Entity or branch not found"},"409":{"description":"Transfer already pending"}}},"get":{"tags":["Branch Transfers"],"summary":"List transfers (filterable by status, entity_type)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["requested","approved_source","approved_target","completed","rejected"]}},{"in":"query","name":"entity_type","schema":{"type":"string","enum":["student","staff"]}}],"responses":{"200":{"description":"List of transfers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/branch-transfers/{id}/approve":{"put":{"tags":["Branch Transfers"],"summary":"Approve a transfer (advances state machine)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Transfer approved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Invalid state transition"},"404":{"description":"Not found"}}}},"/v1/branch-transfers/{id}/reject":{"put":{"tags":["Branch Transfers"],"summary":"Reject a transfer","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Transfer rejected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Already completed or rejected"},"404":{"description":"Not found"}}}},"/v1/branch-policies":{"get":{"tags":["Branch Policies"],"summary":"List all branch policies","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"branch_id","schema":{"type":"string"}},{"in":"query","name":"policy_type","schema":{"type":"string"}}],"responses":{"200":{"description":"List of branch policies","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"post":{"tags":["Branch Policies"],"summary":"Create a branch policy (Owner pushes to all branches when set_by_owner=true)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["branch_id","policy_type","policy_data"],"properties":{"branch_id":{"type":"string"},"policy_type":{"type":"string"},"policy_data":{"type":"object"},"set_by_owner":{"type":"boolean"},"override_allowed":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Created policy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/branch-policies/{id}":{"put":{"tags":["Branch Policies"],"summary":"Update a branch policy","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"policy_data":{"type":"object"},"override_allowed":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated policy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found"}}}},"/v1/branch-policies/{id}/override":{"post":{"tags":["Branch Policies"],"summary":"Request an override for a branch policy","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["branch_id","override_data"],"properties":{"branch_id":{"type":"string"},"override_data":{"type":"object"}}}}}},"responses":{"200":{"description":"Override requested","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Override not allowed"},"404":{"description":"Policy not found"}}}},"/v1/branch-overrides/{id}/approve":{"put":{"tags":["Branch Policies"],"summary":"Approve or reject a branch override","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["approved","rejected"]}}}}}},"responses":{"200":{"description":"Override updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Already processed"},"404":{"description":"Not found"}}}},"/v1/branches":{"get":{"tags":["Branches"],"summary":"List all branches","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List of branches","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Unauthenticated"},"403":{"description":"Missing permission"}}},"post":{"tags":["Branches"],"summary":"Create a new branch","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","code"],"properties":{"name":{"type":"string"},"code":{"type":"string"},"address":{"type":"string"},"phone":{"type":"string"},"is_main":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Created branch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"409":{"description":"Branch code already exists"}}}},"/v1/branches/{id}":{"get":{"tags":["Branches"],"summary":"Get a single branch","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Branch detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found"}}},"put":{"tags":["Branches"],"summary":"Update a branch","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"address":{"type":"string"},"phone":{"type":"string"},"is_active":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated branch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found"}}},"delete":{"tags":["Branches"],"summary":"Soft-delete a branch","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Branch deleted"},"400":{"description":"Cannot delete main branch"},"404":{"description":"Not found"}}}},"/v1/branches/dashboard":{"get":{"tags":["Branches"],"summary":"Aggregated metrics across all branches","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Dashboard metrics","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/branches/{id}/dashboard":{"get":{"tags":["Branches"],"summary":"Per-branch metrics drill-down","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Branch dashboard","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Branch not found"}}}},"/v1/branches/compare":{"get":{"tags":["Branches"],"summary":"Side-by-side comparison of selected branches","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"branch_ids","required":true,"schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true}],"responses":{"200":{"description":"Comparison result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/board-configs":{"get":{"tags":["Board Configs"],"summary":"List all board configurations","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List of board configs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"403":{"description":"Missing permission","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/board-configs/{id}":{"get":{"tags":["Board Configs"],"summary":"Get a single board configuration","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Board config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Board Configs"],"summary":"Update a board configuration","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"board_name":{"type":"string"},"subjects_template":{"type":"object"},"report_card_format":{"type":"object"},"tc_format":{"type":"object"}}}}}},"responses":{"200":{"description":"Updated board config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/board-configs/{id}/activate":{"post":{"tags":["Board Configs"],"summary":"Activate a board configuration (multi-board supported)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Activated board config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/board-configs/{id}/deactivate":{"post":{"tags":["Board Configs"],"summary":"Deactivate a board configuration","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deactivated board config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/board-configs/provision":{"post":{"tags":["Board Configs"],"summary":"Provision board configs during tenant setup (auto-load)","description":"Called during tenant provisioning (M0) to activate board configurations based on the selected board(s). Accepts an array of board codes.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["board_codes"],"properties":{"board_codes":{"type":"array","items":{"type":"string"},"example":["CBSE","ICSE"]}}}}}},"responses":{"200":{"description":"Provisioned board configs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Board code not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/board-configs/{id}/sections":{"post":{"tags":["Board Configs"],"summary":"Assign a board config to a section","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["section_id"],"properties":{"section_id":{"type":"string"}}}}}},"responses":{"200":{"description":"Section-board assignment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Board config or section not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/board-configs/{id}/sections/{sectionId}":{"delete":{"tags":["Board Configs"],"summary":"Remove a board config assignment from a section","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"sectionId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Assignment removed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Assignment not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/board-configs/sections/{sectionId}":{"get":{"tags":["Board Configs"],"summary":"List board configs assigned to a section","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"sectionId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Board configs for section","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/bank-masters":{"get":{"tags":["Bank Masters"],"summary":"List banks (search by IFSC/name/branch)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Bank Masters"],"summary":"Create a new bank","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created"}}}},"/v1/bank-masters/ifsc/{ifsc}":{"get":{"tags":["Bank Masters"],"summary":"Lookup a bank by IFSC code (returns null if not found)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"ifsc","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Bank or null","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/bank-masters/{id}":{"get":{"tags":["Bank Masters"],"summary":"Get a bank by ID","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Bank","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"put":{"tags":["Bank Masters"],"summary":"Update a bank (IFSC cannot be changed)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated"}}},"delete":{"tags":["Bank Masters"],"summary":"Soft-delete a bank","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"}}}},"/v1/auth/otp/request":{"post":{"tags":["Auth"],"summary":"Request an OTP for phone-based authentication","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["phone"],"properties":{"phone":{"type":"string"}}}}}},"responses":{"200":{"description":"OTP sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/auth/otp/verify":{"post":{"tags":["Auth"],"summary":"Verify OTP and receive JWT + refresh token","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["phone","otp"],"properties":{"phone":{"type":"string"},"otp":{"type":"string","minLength":6,"maxLength":6}}}}}},"responses":{"200":{"description":"Login successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Invalid OTP","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/auth/login":{"post":{"tags":["Auth"],"summary":"Login with password (+ OTP if new device)","description":"Step 1: send phone + password. If device is known, returns tokens. If new device, returns `requires_otp: true` and sends OTP. Step 2: resend with phone + password + otp to complete login.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["phone","password"],"properties":{"phone":{"type":"string"},"password":{"type":"string"},"otp":{"type":"string","minLength":6,"maxLength":6}}}}}},"responses":{"200":{"description":"Login successful or OTP challenge","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/auth/devices":{"get":{"tags":["Auth"],"summary":"List trusted devices for the authenticated user","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Device list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/auth/devices/{id}":{"delete":{"tags":["Auth"],"summary":"Revoke trust for a device","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Device revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Device not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/auth/refresh":{"post":{"tags":["Auth"],"summary":"Rotate refresh token and issue new JWT","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["refresh_token"],"properties":{"refresh_token":{"type":"string"}}}}}},"responses":{"200":{"description":"New token pair","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Invalid refresh token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/auth/logout":{"post":{"tags":["Auth"],"summary":"Logout current session (blacklist JWT + revoke refresh token)","security":[{"bearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"refresh_token":{"type":"string"}}}}}},"responses":{"200":{"description":"Logged out","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/auth/logout-all":{"post":{"tags":["Auth"],"summary":"Logout all sessions (revoke all refresh tokens)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"All sessions revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/auth/impersonate":{"post":{"tags":["Auth"],"summary":"Start an impersonation session","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["target_user_id","reason"],"properties":{"target_user_id":{"type":"string"},"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Impersonation started","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not authorized to impersonate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/auth/impersonate/end":{"post":{"tags":["Auth"],"summary":"End an impersonation session","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["session_id"],"properties":{"session_id":{"type":"string"}}}}}},"responses":{"200":{"description":"Impersonation ended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/auth/me":{"get":{"tags":["Auth"],"summary":"Get the currently authenticated user","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Current user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/auth/sign-out":{"post":{"tags":["Auth"],"summary":"Invalidate the current JWT (legacy — prefer /logout)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Logged out","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/auth/forgot-password":{"post":{"tags":["Auth"],"summary":"Request a password-reset OTP (sent to registered email)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["school_code","email"],"properties":{"school_code":{"type":"string"},"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"OTP sent (or silently succeeded)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/auth/verify-reset-otp":{"post":{"tags":["Auth"],"summary":"Verify password-reset OTP and receive a short-lived resetToken","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["school_code","email","otp"],"properties":{"school_code":{"type":"string"},"email":{"type":"string","format":"email"},"otp":{"type":"string","minLength":6,"maxLength":6}}}}}},"responses":{"200":{"description":"OTP verified — resetToken returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Invalid or expired OTP","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/auth/reset-password":{"post":{"tags":["Auth"],"summary":"Set a new password using the resetToken from verify-reset-otp","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["school_code","email","reset_token","new_password"],"properties":{"school_code":{"type":"string"},"email":{"type":"string","format":"email"},"reset_token":{"type":"string"},"new_password":{"type":"string","minLength":6}}}}}},"responses":{"200":{"description":"Password reset successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Invalid or expired reset token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/auth/change-password":{"post":{"tags":["Auth"],"security":[{"bearerAuth":[]}],"summary":"Change own password (authenticated). current_password optional if mustResetPassword=true.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["new_password"],"properties":{"current_password":{"type":"string"},"new_password":{"type":"string","minLength":6}}}}}},"responses":{"200":{"description":"Password changed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Current password incorrect or missing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/approval-chains":{"get":{"tags":["Approval Chains"],"summary":"List approval chain steps (filterable by entity_type, action, branch_id)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":200}},{"in":"query","name":"entity_type","schema":{"type":"string"},"description":"Filter by entity type (e.g., fee_structure, leave, scholarship)"},{"in":"query","name":"action","schema":{"type":"string"},"description":"Filter by action (e.g., approve, review)"},{"in":"query","name":"branch_id","schema":{"type":"string"}}],"responses":{"200":{"description":"List of approval chain steps","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"403":{"description":"Missing permission","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"post":{"tags":["Approval Chains"],"summary":"Create a new approval chain step","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["entity_type","action","step_order","approver_role_id"],"properties":{"entity_type":{"type":"string","description":"e.g., fee_structure, leave, scholarship, circular"},"action":{"type":"string","description":"e.g., approve, review, verify"},"step_order":{"type":"integer","minimum":1},"approver_role_id":{"type":"string"},"threshold_amount":{"type":"number","minimum":0},"branch_id":{"type":"string"}}}}}},"responses":{"201":{"description":"Created approval chain step","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Approver role or branch not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/approval-chains/{id}":{"get":{"tags":["Approval Chains"],"summary":"Get a single approval chain step","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Approval chain step","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Approval Chains"],"summary":"Update an approval chain step","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"entity_type":{"type":"string"},"action":{"type":"string"},"step_order":{"type":"integer","minimum":1},"approver_role_id":{"type":"string"},"threshold_amount":{"type":"number","nullable":true},"branch_id":{"type":"string","nullable":true}}}}}},"responses":{"200":{"description":"Updated approval chain step","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"delete":{"tags":["Approval Chains"],"summary":"Soft-delete an approval chain step","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/approval-chains/next-approver":{"get":{"tags":["Approval Chains"],"summary":"Get the next approver role for a given entity type, action, and current step","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"entity_type","required":true,"schema":{"type":"string"}},{"in":"query","name":"action","required":true,"schema":{"type":"string"}},{"in":"query","name":"current_step","required":true,"schema":{"type":"integer","minimum":0}}],"responses":{"200":{"description":"Next approver info (null if no more steps)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/admissions/enquiries/summary":{"get":{"tags":["Admissions"],"summary":"Get enquiry counts by status","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"branch_id","schema":{"type":"string"}}],"responses":{"200":{"description":"Enquiry summary counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/admissions/applications/summary":{"get":{"tags":["Admissions"],"summary":"Get application counts by pipeline stage","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"branch_id","schema":{"type":"string"}}],"responses":{"200":{"description":"Application summary counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/admissions/fee-config":{"get":{"summary":"Resolve the admission fee configured for a class","description":"Returns the admission fee amount from the fee structure matching the given\nbranch, academic year and class. A fee structure is defined per class, so\n`class_id` should be supplied to get the applicant's actual fee — it is\noptional for backward compatibility, but when omitted the most recently\ncreated structure for the branch is used, which may belong to another class.\nReturns `admission_fee_amount: 0` when no matching structure exists or the\nstructure has no admission fee head.\n","tags":["Admissions"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"branch_id","required":false,"schema":{"type":"string"},"description":"Branch ID (BigInt as string). Defaults to the caller's branch."},{"in":"query","name":"academic_year_id","required":false,"schema":{"type":"string"},"description":"Academic year ID (BigInt as string)"},{"in":"query","name":"class_id","required":false,"schema":{"type":"string"},"description":"Class ID (BigInt as string) — selects that class's fee structure"}],"responses":{"200":{"description":"Admission fee configuration","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/academic-years":{"get":{"tags":["Academics"],"summary":"List academic years","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"is_active","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Academic year list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Academics"],"summary":"Create academic year","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","start_date","end_date"]}}}},"responses":{"200":{"description":"Created academic year"}}}},"/v1/academic-years/{id}":{"get":{"tags":["Academics"],"summary":"Get academic year by ID","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Academic year detail"}}},"put":{"tags":["Academics"],"summary":"Update academic year","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated academic year"}}},"delete":{"tags":["Academics"],"summary":"Soft-delete an academic year","description":"Refused when the year is active or when any records (enrollments, students, fee slips, fee structures, timetables, exams, marks, attendance, applications) reference it. Soft delete only — the row is kept with a deleted_at timestamp.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Academic year deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Academic year not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"422":{"description":"Year is active or has related records","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/classes":{"get":{"tags":["Academics"],"summary":"List academic classes","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"academic_year_id","schema":{"type":"string"}},{"in":"query","name":"branch_id","schema":{"type":"string"}}],"responses":{"200":{"description":"Class list"}}}},"/v1/classes/{id}/delete-impact":{"get":{"tags":["Academics"],"summary":"Preview what deleting a class would remove (and whether it is blocked)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ blocked, student_count, seat_allocation_count, section_count, config_count }"}}}},"/v1/classes/{id}":{"delete":{"tags":["Academics"],"summary":"Soft-delete a class and cascade its config (blocked if students are enrolled)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Class deleted"},"409":{"description":"Class has enrolled students or reserved seats"}}}},"/v1/sections":{"get":{"tags":["Academics"],"summary":"List sections","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"class_id","schema":{"type":"string"}}],"responses":{"200":{"description":"Section list"}}}},"/v1/sections/{id}/delete-impact":{"get":{"tags":["Academics"],"summary":"Preview what deleting a section would remove (and whether it is blocked)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ blocked, student_count, seat_allocation_count, config_count }"}}}},"/v1/sections/{id}":{"delete":{"tags":["Academics"],"summary":"Soft-delete a section and cascade its config (blocked if students are enrolled)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Section deleted"},"409":{"description":"Section has enrolled students or reserved seats"}}}},"/v1/subjects":{"get":{"tags":["Academics"],"summary":"List subjects","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Subject list"}}}},"/platform/v1/schools/{school_id}/setup-snapshot":{"get":{"tags":["Platform - Master Data"],"summary":"List tenant master snapshots for a school","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"school_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"List of snapshots","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Invalid school_id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/platform/v1/master-data/role-templates":{"get":{"tags":["Platform - Master Data"],"summary":"List role templates (paginated + searchable, optional scope filter)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Platform - Master Data"],"summary":"Create a role template","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"409":{"description":"Code already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/platform/v1/master-data/role-templates/{id}":{"get":{"tags":["Platform - Master Data"],"summary":"Get one role template","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Role template","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Platform - Master Data"],"summary":"Update a role template","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"delete":{"tags":["Platform - Master Data"],"summary":"Soft-delete a role template (system templates cannot be deleted)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"System template not deletable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/platform/v1/master-data/pincodes":{"get":{"tags":["Platform - Master Data"],"summary":"List pincodes","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Platform - Master Data"],"summary":"Create a pincode","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/platform/v1/master-data/pincodes/{id}":{"get":{"tags":["Platform - Master Data"],"summary":"Get one pincode","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Pincode","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Platform - Master Data"],"summary":"Update a pincode","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"delete":{"tags":["Platform - Master Data"],"summary":"Soft-delete a pincode","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/platform/v1/master-data/lookups":{"get":{"tags":["Platform - Master Data"],"summary":"List tenant-default lookups (scoped, filterable by type)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"type","schema":{"type":"string"}}],"responses":{"200":{"description":"List","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Platform - Master Data"],"summary":"Create a tenant-default lookup","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/platform/v1/master-data/lookups/{id}":{"get":{"tags":["Platform - Master Data"],"summary":"Get one lookup","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Lookup","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Platform - Master Data"],"summary":"Update a tenant-default lookup","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"delete":{"tags":["Platform - Master Data"],"summary":"Soft-delete a tenant-default lookup","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/platform/v1/master-data/leave-types":{"get":{"tags":["Platform - Master Data"],"summary":"List leave types (paginated + searchable)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":200}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"active_only","schema":{"type":"boolean"}}],"responses":{"200":{"description":"List of leave types","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Platform - Master Data"],"summary":"Create a leave type","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/platform/v1/master-data/leave-types/{id}":{"get":{"tags":["Platform - Master Data"],"summary":"Get a single leave type by id","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Leave type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Platform - Master Data"],"summary":"Update a leave type","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"delete":{"tags":["Platform - Master Data"],"summary":"Soft-delete a leave type","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/platform/v1/master-data/holidays":{"get":{"tags":["Platform - Master Data"],"summary":"List holidays (filterable by academic_year_label)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"academic_year_label","schema":{"type":"string"}}],"responses":{"200":{"description":"List","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Platform - Master Data"],"summary":"Create a holiday","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/platform/v1/master-data/holidays/{id}":{"get":{"tags":["Platform - Master Data"],"summary":"Get one holiday","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Holiday","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Platform - Master Data"],"summary":"Update a holiday","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"delete":{"tags":["Platform - Master Data"],"summary":"Soft-delete a holiday","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/platform/v1/master/fee-heads":{"get":{"tags":["Platform - Master Data"],"summary":"List fee heads (paginated + searchable)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":200}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"active_only","schema":{"type":"boolean"}}],"responses":{"200":{"description":"List of fee heads","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Platform - Master Data"],"summary":"Create a fee head","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","fee_type"],"properties":{"name":{"type":"string"},"fee_type":{"type":"string","enum":["recurring","one_time"]},"description":{"type":"string","nullable":true},"is_system":{"type":"boolean"},"is_new_student_only":{"type":"boolean"},"default_on":{"type":"boolean"},"is_active":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Fee head created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/platform/v1/master/fee-heads/{id}":{"get":{"tags":["Platform - Master Data"],"summary":"Get a single fee head by id","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Fee head","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Platform - Master Data"],"summary":"Update a fee head","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"fee_type":{"type":"string","enum":["recurring","one_time"]},"description":{"type":"string","nullable":true},"is_system":{"type":"boolean"},"is_new_student_only":{"type":"boolean"},"default_on":{"type":"boolean"},"is_active":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Fee head updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"delete":{"tags":["Platform - Master Data"],"summary":"Soft-delete a fee head","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Fee head deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/platform/v1/master-data/document-types":{"get":{"tags":["Platform - Master Data"],"summary":"List document types","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Platform - Master Data"],"summary":"Create a document type","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/platform/v1/master-data/document-types/{id}":{"get":{"tags":["Platform - Master Data"],"summary":"Get one document type","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Document type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Platform - Master Data"],"summary":"Update a document type","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"delete":{"tags":["Platform - Master Data"],"summary":"Soft-delete a document type","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/platform/v1/master-data/designations":{"get":{"tags":["Platform - Master Data"],"summary":"List designations","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Platform - Master Data"],"summary":"Create a designation","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/platform/v1/master-data/designations/{id}":{"get":{"tags":["Platform - Master Data"],"summary":"Get one designation","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Designation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Platform - Master Data"],"summary":"Update a designation","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"delete":{"tags":["Platform - Master Data"],"summary":"Soft-delete a designation","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/platform/v1/master-data/departments":{"get":{"tags":["Platform - Master Data"],"summary":"List departments (paginated + searchable)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Platform - Master Data"],"summary":"Create a department","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/platform/v1/master-data/departments/{id}":{"get":{"tags":["Platform - Master Data"],"summary":"Get one department","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Department","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Platform - Master Data"],"summary":"Update a department","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"delete":{"tags":["Platform - Master Data"],"summary":"Soft-delete a department","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}}}}