openapi: 3.1.0
info:
  title: Catalog Scolar API
  version: 1.0.0
  description: API v1 for the school catalog, Filament admin, and mobile clients. Successful API responses use the standard `{ data, meta, errors }` envelope. Laravel validation errors may use Laravel's native `{ message, errors }` structure when validation fails before controller code runs.
servers:
  - url: https://catalog.hetmann.dev/api/v1
security:
  - bearerAuth: []
tags:
  - name: Auth
  - name: Mobile
  - name: Catalog
  - name: Classes
  - name: Communication
  - name: Reports
  - name: Compliance
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Sanctum personal access token
  schemas:
    ApiEnvelope:
      type: object
      required: [data, meta, errors]
      properties:
        data:
          description: Endpoint payload. It can be an object, an array, or null depending on the operation.
        meta:
          description: Request metadata such as pagination, locale, cache, or session details. It can be null.
        errors:
          description: Null for successful responses; object or array for controlled application errors.
          oneOf:
            - type: "null"
            - type: object
            - type: array
    ValidationError:
      type: object
      required: [message, errors]
      properties:
        message:
          type: string
          example: The given data was invalid.
        errors:
          type: object
          additionalProperties:
            type: array
            items: { type: string }
          example:
            email:
              - Credentialele sunt invalide.
    ErrorEnvelope:
      type: object
      required: [data, meta, errors]
      properties:
        data:
          type: "null"
        meta:
          type: object
          nullable: true
          properties:
            locale: { type: string, example: ro }
        errors:
          type: object
          required: [message]
          properties:
            message: { type: string }
            code: { type: string }
    PaginationMeta:
      type: object
      properties:
        current_page: { type: integer, example: 1 }
        last_page: { type: integer, example: 3 }
        total: { type: integer, example: 52 }
    LocaleMeta:
      type: object
      properties:
        locale: { type: string, enum: [ro, en], example: ro }
    SessionMeta:
      allOf:
        - $ref: "#/components/schemas/LocaleMeta"
        - type: object
          properties:
            session:
              type: object
              properties:
                strategy: { type: string, example: sanctum_personal_access_token }
                expires_at:
                  type: string
                  format: date-time
                  nullable: true
    User:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer, example: 1 }
        name: { type: string, example: Popescu Ana }
        email: { type: string, format: email, example: ana.popescu@example.test }
        roles:
          type: array
          items: { type: string }
          example: [profesor]
        active_role: { type: string, nullable: true, example: profesor }
    TeacherProfile:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        user_id: { type: integer }
        employee_number: { type: string, nullable: true }
    StudentProfile:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer, example: 14 }
        first_name: { type: string, example: Maria }
        last_name: { type: string, example: Ionescu }
        national_id: { type: string, nullable: true }
        registration_number: { type: string, nullable: true }
    GuardianProfile:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        user_id: { type: integer }
        phone: { type: string, nullable: true }
        students:
          type: array
          items: { $ref: "#/components/schemas/StudentProfile" }
    SchoolYear:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        name: { type: string, example: 2026-2027 }
        starts_on: { type: string, format: date }
        ends_on: { type: string, format: date }
    ClassGroup:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        name: { type: string, example: IX A }
        education_level: { type: string, example: liceal }
        school_year: { $ref: "#/components/schemas/SchoolYear" }
    Subject:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        name: { type: string, example: Matematica }
        code: { type: string, nullable: true }
    ClassSubject:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        class_group_id: { type: integer }
        subject_id: { type: integer }
        subject: { $ref: "#/components/schemas/Subject" }
    Grade:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer, example: 101 }
        student_profile_id: { type: integer, example: 14 }
        class_subject_id: { type: integer, example: 7 }
        teacher_profile_id: { type: integer, example: 2 }
        value: { type: integer, minimum: 1, maximum: 10, example: 9 }
        type: { type: string, nullable: true, example: oral }
        graded_at: { type: string, format: date-time }
        notes: { type: string, nullable: true }
        class_subject: { $ref: "#/components/schemas/ClassSubject" }
    Absence:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer, example: 205 }
        student_profile_id: { type: integer, example: 14 }
        class_subject_id: { type: integer, example: 7 }
        teacher_profile_id: { type: integer, example: 2 }
        absent_on: { type: string, format: date, example: 2026-10-14 }
        status: { type: string, enum: [unexcused, excused, pending], example: unexcused }
        notes: { type: string, nullable: true }
        class_subject: { $ref: "#/components/schemas/ClassSubject" }
        justification: { $ref: "#/components/schemas/AbsenceJustification" }
    AbsenceJustification:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        absence_id: { type: integer }
        reason: { type: string, example: Adeverinta medicala }
        document_path: { type: string, nullable: true }
        approved_by: { type: integer, nullable: true }
        approved_at: { type: string, format: date-time, nullable: true }
    AgendaItem:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        class_group_id: { type: integer }
        student_profile_id: { type: integer, nullable: true }
        subject_id: { type: integer, nullable: true }
        type: { type: string, enum: [homework, test, announcement, meeting] }
        title: { type: string }
        body: { type: string, nullable: true }
        due_on: { type: string, format: date, nullable: true }
    TimetableEntry:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        class_group_id: { type: integer }
        subject_id: { type: integer }
        teacher_profile_id: { type: integer }
        weekday: { type: integer, minimum: 1, maximum: 7 }
        starts_at: { type: string, example: "08:00:00" }
        ends_at: { type: string, example: "08:50:00" }
        subject: { $ref: "#/components/schemas/Subject" }
    MobileAppConfig:
      type: object
      required: [brand, theme, assets, links, support, localization, features, stores, versioning, maintenance, updated_at]
      properties:
        brand:
          type: object
          properties:
            app_name: { type: string, example: Catalog Scolar }
            short_name: { type: string, example: Catalog }
            school_name: { type: string, example: Liceul Teoretic Virgil Madgearu Cluj-Napoca }
            organization_name: { type: string, nullable: true }
        theme:
          type: object
          properties:
            primary_color: { type: string, example: "#2563eb" }
            secondary_color: { type: string, example: "#0f172a" }
            accent_color: { type: string, example: "#16a34a" }
            background_color: { type: string, example: "#ffffff" }
            text_color: { type: string, example: "#111827" }
        assets:
          type: object
          properties:
            logo_url: { type: string, format: uri, nullable: true }
            icon_url: { type: string, format: uri, nullable: true }
            splash_image_url: { type: string, format: uri, nullable: true }
        links:
          type: object
          properties:
            website_url: { type: string, format: uri, nullable: true }
            privacy_policy_url: { type: string, format: uri, nullable: true }
            terms_url: { type: string, format: uri, nullable: true }
        support:
          type: object
          properties:
            email: { type: string, format: email, nullable: true }
            phone: { type: string, nullable: true }
        localization:
          type: object
          properties:
            default_locale: { type: string, enum: [ro, en] }
            enabled_locales:
              type: array
              items: { type: string, enum: [ro, en] }
        features:
          type: object
          additionalProperties: { type: boolean }
          example:
            grades: true
            absences: true
            messages: true
        stores:
          type: object
          properties:
            ios_bundle_id: { type: string, nullable: true }
            ios_app_store_id: { type: string, nullable: true }
            android_package_name: { type: string, nullable: true }
        versioning:
          type: object
          properties:
            minimum_ios_version: { type: string, nullable: true }
            minimum_android_version: { type: string, nullable: true }
        maintenance:
          type: object
          properties:
            enabled: { type: boolean }
            message: { type: string, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }
    MessageThread:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        subject: { type: string }
        created_by: { type: integer }
        participants:
          type: array
          items: { $ref: "#/components/schemas/MessageParticipant" }
        messages:
          type: array
          items: { $ref: "#/components/schemas/Message" }
    MessageParticipant:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        user_id: { type: integer }
        last_read_at: { type: string, format: date-time, nullable: true }
        user: { $ref: "#/components/schemas/User" }
    Message:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        message_thread_id: { type: integer }
        sender_id: { type: integer }
        body: { type: string }
        sent_at: { type: string, format: date-time }
        sender: { $ref: "#/components/schemas/User" }
    SupportTicket:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        audience: { type: string, enum: [parent, teacher, student, staff] }
        category: { type: string, enum: [general, technical, gdpr, catalog] }
        priority: { type: string, enum: [low, normal, high, urgent] }
        subject: { type: string }
        created_by: { type: integer }
        assigned_to: { type: integer, nullable: true }
        messages:
          type: array
          items: { $ref: "#/components/schemas/SupportTicketMessage" }
    SupportTicketMessage:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        support_ticket_id: { type: integer }
        sender_id: { type: integer }
        body: { type: string }
        sender: { $ref: "#/components/schemas/User" }
    DeviceToken:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        user_id: { type: integer }
        token: { type: string }
        platform: { type: string, enum: [ios, android, web, unknown] }
        device_name: { type: string, nullable: true }
        last_seen_at: { type: string, format: date-time }
    NotificationPreference:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        user_id: { type: integer }
        channel: { type: string, enum: [push, email] }
        event: { type: string, enum: [grade_created, absence_created, status_closed] }
        enabled: { type: boolean }
    ReportSnapshot:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        type: { type: string, enum: [monthly_absenteeism, annual_summary, pnras_risk] }
        school_year_id: { type: integer }
        class_group_id: { type: integer, nullable: true }
        generated_by: { type: integer }
        generated_at: { type: string, format: date-time }
        payload: { type: object, additionalProperties: true }
    ExportDocument:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        type: { type: string, example: class-catalog }
        path: { type: string }
        hash: { type: string }
        generated_by: { type: integer }
        generated_at: { type: string, format: date-time }
    GdprRequest:
      type: object
      additionalProperties: true
      properties:
        id: { type: integer }
        requester_id: { type: integer }
        student_profile_id: { type: integer, nullable: true }
        type: { type: string, enum: [access, rectification, restriction, portability] }
        details: { type: string, nullable: true }
    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email: { type: string, format: email }
        password: { type: string, format: password }
        device_name: { type: string, maxLength: 120, nullable: true }
    OtpVerifyRequest:
      type: object
      required: [user_id, code]
      properties:
        user_id: { type: integer }
        code: { type: string, minLength: 6, maxLength: 6, example: "123456" }
        device_name: { type: string, maxLength: 120, nullable: true }
  responses:
    Unauthorized:
      description: Missing, invalid, or expired bearer token.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
          example:
            data: null
            meta: { locale: ro }
            errors: { message: Neautentificat., code: unauthenticated }
    Forbidden:
      description: The user is authenticated but is not allowed to access or modify this resource.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
          example:
            data: null
            meta: { locale: ro }
            errors: { message: Aceasta actiune nu este permisa., code: forbidden }
    NotFound:
      description: The resource does not exist or is not visible to the current user.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
          example:
            data: null
            meta: { locale: ro }
            errors: { message: Resource not found., code: not_found }
    ValidationError:
      description: Input validation failed. Laravel returns nested keys using dot notation.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ValidationError" }
    RateLimited:
      description: Too many requests. The client should slow down or retry after the rate-limit window resets.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
          example:
            data: null
            meta: { locale: ro }
            errors: { message: Too Many Attempts., code: rate_limited }
  requestBodies:
    GradeCreate:
      required: true
      content:
        application/json:
          schema:
            type: object
            required: [student_profile_id, value, graded_at]
            properties:
              student_profile_id: { type: integer }
              value: { type: integer, minimum: 1, maximum: 10 }
              type: { type: string, maxLength: 50, nullable: true }
              graded_at: { type: string, format: date }
              notes: { type: string, nullable: true }
          example:
            student_profile_id: 14
            value: 9
            type: oral
            graded_at: 2026-10-14
            notes: Raspuns foarte bun.
    GradeUpdate:
      required: true
      content:
        application/json:
          schema:
            type: object
            required: [reason]
            properties:
              value: { type: integer, minimum: 1, maximum: 10 }
              type: { type: string, maxLength: 50 }
              graded_at: { type: string, format: date }
              notes: { type: string, nullable: true }
              reason: { type: string, maxLength: 500 }
          example:
            value: 10
            reason: Corectie dupa contestatie interna.
    AbsenceCreate:
      required: true
      content:
        application/json:
          schema:
            type: object
            required: [student_profile_id, absent_on]
            properties:
              student_profile_id: { type: integer }
              absent_on: { type: string, format: date }
              status: { type: string, enum: [unexcused, excused, pending], nullable: true }
              notes: { type: string, nullable: true }
          example:
            student_profile_id: 14
            absent_on: 2026-10-14
            status: unexcused
    AbsenceUpdate:
      required: true
      content:
        application/json:
          schema:
            type: object
            required: [reason]
            properties:
              absent_on: { type: string, format: date }
              status: { type: string, enum: [unexcused, excused, pending] }
              notes: { type: string, nullable: true }
              reason: { type: string, maxLength: 500 }
          example:
            status: excused
            reason: Motivata pe baza adeverintei medicale.
paths:
  /auth/login:
    post:
      tags: [Auth]
      security: []
      summary: "/auth/login"
      description: "Login with email and password. Returns a Sanctum token for standard users or starts the 2FA flow for privileged roles."
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LoginRequest" }
            examples:
              standard:
                value:
                  email: profesor@example.test
                  password: password
                  device_name: iPhone 16
      responses:
        "200":
          description: Authentication succeeded, or the account requires a second factor before a token can be issued.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        oneOf:
                          - type: object
                            required: [token, token_type]
                            properties:
                              token: { type: string }
                              token_type: { type: string, example: Bearer }
                          - type: object
                            required: [requires_2fa, user_id]
                            properties:
                              requires_2fa: { type: boolean, const: true }
                              user_id: { type: integer }
                      meta: { $ref: "#/components/schemas/SessionMeta" }
              examples:
                token:
                  value:
                    data: { token: "1|plainTextToken", token_type: Bearer }
                    meta: { locale: ro, session: { strategy: sanctum_personal_access_token, expires_at: null } }
                    errors: null
                requires_2fa:
                  value:
                    data: { requires_2fa: true, user_id: 1 }
                    meta: { message: Codul de verificare a fost trimis., locale: ro }
                    errors: null
        "422": { $ref: "#/components/responses/ValidationError" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /auth/2fa/verify:
    post:
      tags: [Auth]
      security: []
      summary: "/auth/2fa/verify"
      description: "Verify the email OTP code and issue a Sanctum bearer token."
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OtpVerifyRequest" }
      responses:
        "200":
          description: A bearer token is issued after the OTP code is validated.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [token, token_type]
                        properties:
                          token: { type: string }
                          token_type: { type: string, example: Bearer }
                      meta: { $ref: "#/components/schemas/SessionMeta" }
              example:
                data: { token: "1|plainTextToken", token_type: Bearer }
                meta: { locale: ro, session: { strategy: sanctum_personal_access_token, expires_at: null } }
                errors: null
        "422": { $ref: "#/components/responses/ValidationError" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /auth/2fa/resend:
    post:
      tags: [Auth]
      security: []
      summary: "/auth/2fa/resend"
      description: "Resend the OTP code for a pending privileged login."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_id]
              properties:
                user_id: { type: integer }
      responses:
        "200":
          description: The OTP code was resent for the selected user.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [requires_2fa, user_id]
                        properties:
                          requires_2fa: { type: boolean, const: true }
                          user_id: { type: integer }
                      meta: { $ref: "#/components/schemas/LocaleMeta" }
              example:
                data: { requires_2fa: true, user_id: 1 }
                meta: { message: Codul de verificare a fost retrimis., locale: ro }
                errors: null
        "422": { $ref: "#/components/responses/ValidationError" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /auth/logout:
    post:
      tags: [Auth]
      summary: "/auth/logout"
      description: "Revoke the current Sanctum token."
      responses:
        "200":
          description: The current token was revoked and can no longer be used.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          revoked: { type: boolean, example: true }
                      meta: { $ref: "#/components/schemas/LocaleMeta" }
              example:
                data: { revoked: true }
                meta: { locale: ro }
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
  /me:
    get:
      tags: [Auth]
      summary: "/me"
      description: "Return the normalized context for the authenticated user."
      responses:
        "200":
          description: Current user details, roles, active role, and related teacher/student/guardian profiles.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        allOf:
                          - $ref: "#/components/schemas/User"
                          - type: object
                            properties:
                              teacher_profile: { $ref: "#/components/schemas/TeacherProfile" }
                              student_profile: { $ref: "#/components/schemas/StudentProfile" }
                              guardian_profile: { $ref: "#/components/schemas/GuardianProfile" }
                      meta:
                        allOf:
                          - $ref: "#/components/schemas/SessionMeta"
                          - type: object
                            properties:
                              legal_mode: { type: string, enum: [dual, official_electronic, internal_support] }
              example:
                data:
                  id: 1
                  name: Profesor Demo
                  email: profesor@example.test
                  roles: [profesor]
                  active_role: profesor
                  teacher_profile: { id: 2, user_id: 1 }
                  student_profile: null
                  guardian_profile: null
                meta: { locale: ro, legal_mode: dual, session: { strategy: sanctum_personal_access_token, expires_at: null } }
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
  /mobile/app-config:
    get:
      tags: [Mobile]
      security: []
      summary: "/mobile/app-config"
      description: "Return the public white-label configuration consumed by mobile apps before or after login."
      parameters:
        - name: locale
          in: query
          schema: { type: string, enum: [ro, en] }
      responses:
        "200":
          description: Branding, theme colors, assets, legal/support links, localization, feature flags, store metadata, versioning, and maintenance state.
          headers:
            Cache-Control:
              schema: { type: string }
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/MobileAppConfig" }
                      meta:
                        allOf:
                          - $ref: "#/components/schemas/LocaleMeta"
                          - type: object
                            properties:
                              api_version: { type: string, example: v1 }
                              cache:
                                type: object
                                properties:
                                  etag: { type: string }
                                  max_age_seconds: { type: integer, example: 300 }
              example:
                data:
                  brand: { app_name: Catalog Scolar, short_name: Catalog, school_name: Liceul Teoretic Virgil Madgearu Cluj-Napoca, organization_name: null }
                  theme: { primary_color: "#2563eb", secondary_color: "#0f172a", accent_color: "#16a34a", background_color: "#ffffff", text_color: "#111827" }
                  assets: { logo_url: null, icon_url: null, splash_image_url: null }
                  links: { website_url: "https://liceulvmadgearucluj.ro/", privacy_policy_url: "https://liceulvmadgearucluj.ro/", terms_url: "https://liceulvmadgearucluj.ro/" }
                  support: { email: contact@example.test, phone: "+40700000000" }
                  localization: { default_locale: ro, enabled_locales: [ro, en] }
                  features: { grades: true, absences: true, messages: true }
                  stores: { ios_bundle_id: null, ios_app_store_id: null, android_package_name: null }
                  versioning: { minimum_ios_version: null, minimum_android_version: null }
                  maintenance: { enabled: false, message: null }
                  updated_at: "2026-06-12T10:00:00.000000Z"
                meta: { locale: en, api_version: v1, cache: { etag: abc123, max_age_seconds: 300 } }
                errors: null
        "429": { $ref: "#/components/responses/RateLimited" }
  /mobile/bootstrap:
    get:
      tags: [Mobile]
      summary: "/mobile/bootstrap"
      description: "Return the authenticated mobile bootstrap payload in a single request."
      responses:
        "200":
          description: User context, guardian children, student profile, teacher assignments, notification preferences, app configuration, and client capabilities.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          user: { $ref: "#/components/schemas/User" }
                          guardian_students:
                            type: array
                            items: { $ref: "#/components/schemas/StudentProfile" }
                          student_profile: { $ref: "#/components/schemas/StudentProfile" }
                          teacher_assignments:
                            type: array
                            items:
                              type: object
                              additionalProperties: true
                          notification_preferences:
                            type: array
                            items: { $ref: "#/components/schemas/NotificationPreference" }
                          app_config: { $ref: "#/components/schemas/MobileAppConfig" }
                          capabilities:
                            type: object
                            properties:
                              offline_cache:
                                type: object
                                properties:
                                  read_only: { type: boolean }
                                  write_requires_network: { type: boolean }
                              uploads:
                                type: object
                                properties:
                                  absence_justification: { type: boolean }
                      meta:
                        allOf:
                          - $ref: "#/components/schemas/LocaleMeta"
                          - type: object
                            properties:
                              supported_locales:
                                type: array
                                items: { type: string }
                              legal_mode: { type: string }
                              api_version: { type: string, example: v1 }
              example:
                data:
                  user: { id: 1, name: Profesor Demo, email: profesor@example.test, roles: [profesor], active_role: profesor }
                  guardian_students: []
                  student_profile: null
                  teacher_assignments: []
                  notification_preferences: []
                  app_config: { brand: { app_name: Catalog Scolar } }
                  capabilities: { offline_cache: { read_only: true, write_requires_network: true }, uploads: { absence_justification: true } }
                meta: { locale: ro, supported_locales: [ro, en], legal_mode: dual, api_version: v1 }
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
  /guardians/me/students:
    get:
      tags: [Catalog]
      summary: "/guardians/me/students"
      description: "List the students visible to the current guardian account."
      responses:
        "200":
          description: Students with their current enrollment and class, scoped to the authenticated guardian.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/StudentProfile" }
              example:
                data: [{ id: 14, first_name: Maria, last_name: Ionescu }]
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
  /teachers/me/classes:
    get:
      tags: [Classes]
      summary: "/teachers/me/classes"
      description: "List the classes assigned to the current teacher."
      responses:
        "200":
          description: Classes where the teacher has at least one assigned subject.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/ClassGroup" }
              example:
                data: [{ id: 3, name: IX A, education_level: liceal, school_year: { id: 1, name: 2026-2027 } }]
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /classes/{class}/students:
    get:
      tags: [Classes]
      summary: "/classes/{class}/students"
      description: "List active students in an assigned class."
      parameters:
        - name: class
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Active students for the selected class.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/StudentProfile" }
              example:
                data: [{ id: 14, first_name: Maria, last_name: Ionescu }]
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /classes/{class}/subjects:
    get:
      tags: [Classes]
      summary: "/classes/{class}/subjects"
      description: "List subjects assigned to the current teacher for a class."
      parameters:
        - name: class
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: List `class_subjects` records including their related `subject` object.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/ClassSubject" }
              example:
                data: [{ id: 7, class_group_id: 3, subject_id: 2, subject: { id: 2, name: Matematica } }]
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /classes/{class}/timetable:
    get:
      tags: [Classes]
      summary: "/classes/{class}/timetable"
      description: "Return the class timetable visible to teachers, guardians, students, directors, and secretariat users."
      parameters:
        - name: class
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Timetable entries ordered by weekday and start time.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/TimetableEntry" }
              example:
                data: [{ id: 1, class_group_id: 3, subject_id: 2, weekday: 1, starts_at: "08:00:00", ends_at: "08:50:00", subject: { id: 2, name: Matematica } }]
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /classes/{class}/agenda:
    get:
      tags: [Classes]
      summary: "/classes/{class}/agenda"
      description: "List class agenda items such as homework, tests, announcements, and meetings."
      parameters:
        - name: class
          in: path
          required: true
          schema: { type: integer }
        - name: per_page
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
      responses:
        "200":
          description: Paginated agenda items for the selected class.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/AgendaItem" }
                      meta:
                        type: object
                        properties:
                          pagination: { $ref: "#/components/schemas/PaginationMeta" }
              example:
                data: [{ id: 1, type: homework, title: Exercitii pagina 42, due_on: 2026-10-20 }]
                meta: { pagination: { current_page: 1, last_page: 1, total: 1 } }
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Classes]
      summary: "/classes/{class}/agenda"
      description: "Create an agenda item for a class as an assigned teacher."
      parameters:
        - name: class
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, title]
              properties:
                student_profile_id: { type: integer, nullable: true }
                subject_id: { type: integer, nullable: true }
                type: { type: string, enum: [homework, test, announcement, meeting] }
                title: { type: string, maxLength: 255 }
                body: { type: string, nullable: true }
                due_on: { type: string, format: date, nullable: true }
            example:
              subject_id: 2
              type: homework
              title: Exercitii pagina 42
              body: Pregatiti exercitiile 1-5.
              due_on: 2026-10-20
      responses:
        "201":
          description: The agenda item was created and linked to the class.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/AgendaItem" }
              example:
                data: { id: 1, class_group_id: 3, subject_id: 2, type: homework, title: Exercitii pagina 42, due_on: 2026-10-20 }
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /students/{student}/catalog:
    get:
      tags: [Catalog]
      summary: "/students/{student}/catalog"
      description: "Return a student catalog with grades and absences, each with separate pagination metadata."
      parameters:
        - name: student
          in: path
          required: true
          schema: { type: integer }
        - name: subject_id
          in: query
          schema: { type: integer }
        - name: date_from
          in: query
          schema: { type: string, format: date }
        - name: date_to
          in: query
          schema: { type: string, format: date }
        - name: per_page
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
      responses:
        "200":
          description: Catalog data for a student the current user is allowed to view.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          student: { $ref: "#/components/schemas/StudentProfile" }
                          grades:
                            type: array
                            items: { $ref: "#/components/schemas/Grade" }
                          absences:
                            type: array
                            items: { $ref: "#/components/schemas/Absence" }
                      meta:
                        type: object
                        properties:
                          locale: { type: string }
                          legal_mode: { type: string }
                          offline_cache:
                            type: object
                            properties:
                              read_only: { type: boolean }
                          pagination:
                            type: object
                            properties:
                              grades: { $ref: "#/components/schemas/PaginationMeta" }
                              absences: { $ref: "#/components/schemas/PaginationMeta" }
              example:
                data:
                  student: { id: 14, first_name: Maria, last_name: Ionescu }
                  grades: [{ id: 101, value: 9, type: oral, graded_at: "2026-10-14T00:00:00.000000Z" }]
                  absences: [{ id: 205, absent_on: 2026-10-14, status: unexcused }]
                meta:
                  locale: ro
                  legal_mode: dual
                  offline_cache: { read_only: true }
                  pagination: { grades: { current_page: 1, last_page: 1, total: 1 }, absences: { current_page: 1, last_page: 1, total: 1 } }
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /classes/{class}/subjects/{subject}/grades:
    post:
      tags: [Catalog]
      summary: "/classes/{class}/subjects/{subject}/grades"
      description: "Create a grade for a subject assigned to the authenticated teacher."
      parameters:
        - name: class
          in: path
          required: true
          schema: { type: integer }
        - name: subject
          in: path
          required: true
          schema: { type: integer }
      requestBody: { $ref: "#/components/requestBodies/GradeCreate" }
      responses:
        "201":
          description: The grade was created, audit logged, and guardian notifications were queued/generated.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Grade" }
              example:
                data: { id: 101, student_profile_id: 14, class_subject_id: 7, teacher_profile_id: 2, value: 9, type: oral, graded_at: "2026-10-14T00:00:00.000000Z" }
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /grades/{grade}:
    patch:
      tags: [Catalog]
      summary: "/grades/{grade}"
      description: "Update a grade. The `reason` field is required and is stored in the audit log."
      parameters:
        - name: grade
          in: path
          required: true
          schema: { type: integer }
      requestBody: { $ref: "#/components/requestBodies/GradeUpdate" }
      responses:
        "200":
          description: The grade was updated and the change was audit logged.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Grade" }
              example:
                data: { id: 101, value: 10, type: oral, notes: Raspuns foarte bun. }
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /classes/{class}/subjects/{subject}/absences:
    post:
      tags: [Catalog]
      summary: "/classes/{class}/subjects/{subject}/absences"
      description: "Create an absence for a subject assigned to the authenticated teacher."
      parameters:
        - name: class
          in: path
          required: true
          schema: { type: integer }
        - name: subject
          in: path
          required: true
          schema: { type: integer }
      requestBody: { $ref: "#/components/requestBodies/AbsenceCreate" }
      responses:
        "201":
          description: The absence was created, audit logged, and guardian notifications were queued/generated.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Absence" }
              example:
                data: { id: 205, student_profile_id: 14, class_subject_id: 7, teacher_profile_id: 2, absent_on: 2026-10-14, status: unexcused }
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /absences/{absence}:
    patch:
      tags: [Catalog]
      summary: "/absences/{absence}"
      description: "Update an absence. The `reason` field is required and is stored in the audit log."
      parameters:
        - name: absence
          in: path
          required: true
          schema: { type: integer }
      requestBody: { $ref: "#/components/requestBodies/AbsenceUpdate" }
      responses:
        "200":
          description: The absence was updated and the change was audit logged.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Absence" }
              example:
                data: { id: 205, absent_on: 2026-10-14, status: excused }
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /absences/{absence}/justification:
    post:
      tags: [Catalog]
      summary: "/absences/{absence}/justification"
      description: "Submit an absence justification with an optional PDF/JPG/PNG document."
      parameters:
        - name: absence
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [reason]
              properties:
                reason: { type: string, maxLength: 1000 }
                document: { type: string, format: binary, nullable: true }
      responses:
        "201":
          description: The justification is created or replaced, and the absence status becomes `pending` until staff review.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/AbsenceJustification" }
              example:
                data: { id: 1, absence_id: 205, reason: Adeverinta medicala, document_path: absence-justifications/file.pdf, approved_by: null, approved_at: null }
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /device-tokens:
    post:
      tags: [Mobile]
      summary: "/device-tokens"
      description: "Register or update the push notification token for the current device."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, platform]
              properties:
                token: { type: string, maxLength: 512 }
                platform: { type: string, enum: [ios, android, web, unknown] }
                device_name: { type: string, maxLength: 120, nullable: true }
            example:
              token: ExpoPushToken[xxxxxxxxxxxxxxxxxxxxxx]
              platform: ios
              device_name: iPhone 16
      responses:
        "201":
          description: Device token upserted.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/DeviceToken" }
              example:
                data: { id: 1, user_id: 1, platform: ios, device_name: iPhone 16, last_seen_at: "2026-06-12T10:00:00.000000Z" }
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /notification-preferences:
    put:
      tags: [Mobile]
      summary: "/notification-preferences"
      description: "Replace or update notification preferences for the authenticated user."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [preferences]
              properties:
                preferences:
                  type: array
                  items:
                    type: object
                    required: [channel, event, enabled]
                    properties:
                      channel: { type: string, enum: [push, email] }
                      event: { type: string, enum: [grade_created, absence_created, status_closed] }
                      enabled: { type: boolean }
            example:
              preferences:
                - { channel: push, event: grade_created, enabled: true }
                - { channel: email, event: absence_created, enabled: false }
      responses:
        "200":
          description: Complete list of notification preferences for the current user.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/NotificationPreference" }
              example:
                data: [{ id: 1, user_id: 1, channel: push, event: grade_created, enabled: true }]
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /messages:
    get:
      tags: [Communication]
      summary: "/messages"
      description: "List message threads where the current user is a participant."
      parameters:
        - name: per_page
          in: query
          schema: { type: integer, minimum: 1, default: 25 }
      responses:
        "200":
          description: Paginated message threads with participants and the latest message loaded.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/MessageThread" }
                      meta:
                        type: object
                        properties:
                          pagination:
                            type: object
                            properties:
                              total: { type: integer }
              example:
                data: [{ id: 1, subject: Sedinta cu parintii, messages: [] }]
                meta: { pagination: { total: 1 } }
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Communication]
      summary: "/messages"
      description: "Create a message thread with participants and an initial message."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [subject, participant_user_ids, body]
              properties:
                subject: { type: string, maxLength: 255 }
                participant_user_ids:
                  type: array
                  minItems: 1
                  items: { type: integer }
                body: { type: string }
            example:
              subject: Sedinta cu parintii
              participant_user_ids: [2, 3]
              body: Buna ziua, va invitam la sedinta.
      responses:
        "201":
          description: The thread was created with the initial message and participant records.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/MessageThread" }
              example:
                data: { id: 1, subject: Sedinta cu parintii, participants: [], messages: [] }
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /messages/{thread}:
    get:
      tags: [Communication]
      summary: "/messages/{thread}"
      description: "Read a message thread and mark the current participant as having read it."
      parameters:
        - name: thread
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Thread details including participants and messages.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/MessageThread" }
              example:
                data: { id: 1, subject: Sedinta cu parintii, participants: [], messages: [] }
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /messages/{thread}/replies:
    post:
      tags: [Communication]
      summary: "/messages/{thread}/replies"
      description: "Reply to a message thread where the current user is a participant."
      parameters:
        - name: thread
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              properties:
                body: { type: string }
            example:
              body: Confirm participarea.
      responses:
        "201":
          description: The reply message was created in the thread.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Message" }
              example:
                data: { id: 10, message_thread_id: 1, sender_id: 1, body: Confirm participarea., sent_at: "2026-06-12T10:00:00.000000Z" }
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /support-tickets:
    get:
      tags: [Communication]
      summary: "/support-tickets"
      description: "List own support tickets, or all support tickets for staff roles."
      parameters:
        - name: per_page
          in: query
          schema: { type: integer, minimum: 1, default: 25 }
      responses:
        "200":
          description: Paginated support tickets visible to the current user.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/SupportTicket" }
                      meta:
                        type: object
                        properties:
                          pagination:
                            type: object
                            properties:
                              total: { type: integer }
              example:
                data: [{ id: 1, audience: parent, category: technical, priority: normal, subject: Nu pot vedea notele }]
                meta: { pagination: { total: 1 } }
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Communication]
      summary: "/support-tickets"
      description: "Create a support ticket with an initial message."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [audience, category, subject, body]
              properties:
                audience: { type: string, enum: [parent, teacher, student, staff] }
                category: { type: string, enum: [general, technical, gdpr, catalog] }
                priority: { type: string, enum: [low, normal, high, urgent], nullable: true }
                subject: { type: string, maxLength: 255 }
                body: { type: string }
            example:
              audience: parent
              category: technical
              priority: normal
              subject: Nu pot vedea notele
              body: Aplicatia nu afiseaza catalogul copilului.
      responses:
        "201":
          description: The support ticket was created with the initial message.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/SupportTicket" }
              example:
                data: { id: 1, audience: parent, category: technical, priority: normal, subject: Nu pot vedea notele, messages: [] }
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /support-tickets/{ticket}:
    get:
      tags: [Communication]
      summary: "/support-tickets/{ticket}"
      description: "Read a support ticket visible to the current user."
      parameters:
        - name: ticket
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Ticket details including creator, assignee, and messages.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/SupportTicket" }
              example:
                data: { id: 1, audience: parent, category: technical, priority: normal, subject: Nu pot vedea notele, messages: [] }
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /support-tickets/{ticket}/replies:
    post:
      tags: [Communication]
      summary: "/support-tickets/{ticket}/replies"
      description: "Reply to a support ticket visible to the current user."
      parameters:
        - name: ticket
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              properties:
                body: { type: string }
            example:
              body: Am verificat si problema este rezolvata.
      responses:
        "201":
          description: The support ticket reply was created.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/SupportTicketMessage" }
              example:
                data: { id: 5, support_ticket_id: 1, sender_id: 1, body: Am verificat si problema este rezolvata. }
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /report-snapshots:
    get:
      tags: [Reports]
      summary: "/report-snapshots"
      description: "List generated management report snapshots."
      parameters:
        - name: per_page
          in: query
          schema: { type: integer, minimum: 1, default: 25 }
      responses:
        "200":
          description: Report snapshots available to director and secretariat roles.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/ReportSnapshot" }
                      meta:
                        type: object
                        properties:
                          pagination:
                            type: object
                            properties:
                              total: { type: integer }
              example:
                data: [{ id: 1, type: monthly_absenteeism, school_year_id: 1, generated_by: 1, payload: {} }]
                meta: { pagination: { total: 1 } }
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Reports]
      summary: "/report-snapshots"
      description: "Generate a report snapshot: `monthly_absenteeism`, `annual_summary`, or `pnras_risk`."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, school_year_id]
              properties:
                type: { type: string, enum: [monthly_absenteeism, annual_summary, pnras_risk] }
                school_year_id: { type: integer }
                class_group_id: { type: integer, nullable: true }
            example:
              type: monthly_absenteeism
              school_year_id: 1
              class_group_id: 3
      responses:
        "201":
          description: The report snapshot was generated and stored.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/ReportSnapshot" }
              example:
                data: { id: 1, type: monthly_absenteeism, school_year_id: 1, class_group_id: 3, generated_by: 1, payload: {} }
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /classes/{class}/exports/catalog:
    post:
      tags: [Reports]
      summary: "/classes/{class}/exports/catalog"
      description: "Generate a hashed PDF export for a class catalog."
      parameters:
        - name: class
          in: path
          required: true
          schema: { type: integer }
      responses:
        "201":
          description: The export document was created and audit logged.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/ExportDocument" }
              example:
                data: { id: 1, type: class-catalog, path: exports/class-catalog.pdf, hash: sha256hash, generated_by: 1, generated_at: "2026-06-12T10:00:00.000000Z" }
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /gdpr-requests:
    post:
      tags: [Compliance]
      summary: "/gdpr-requests"
      description: "Submit a GDPR request for access, rectification, restriction, or portability."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type]
              properties:
                student_profile_id: { type: integer, nullable: true }
                type: { type: string, enum: [access, rectification, restriction, portability] }
                details: { type: string, nullable: true }
            example:
              student_profile_id: 14
              type: access
              details: Solicit export date personale.
      responses:
        "201":
          description: The GDPR request was registered for staff processing.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/GdprRequest" }
              example:
                data: { id: 1, requester_id: 1, student_profile_id: 14, type: access, details: Solicit export date personale. }
                meta: null
                errors: null
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/ValidationError" }
