openapi: 3.0.3
info:
  title: Escape Dental API
  version: 1.0.0
  description: Practice management platform API for Escape Dental.
paths:
  /api/v1/account/invite-codes/:
    get:
      operationId: account_invite_codes_list
      description: |-
        List or mint invite codes.

        GET
            Returns the codes attributed to the caller's accessible clients.
            Staff users may pass ``?client=<uid>`` to scope to one specific
            Client; without the filter, staff see codes for *every* client
            they can access (and superusers see them all).

            Paginated with the project default :class:`StandardPageNumberPagination`
            (20 per page, ``?page_size=`` capped at 100, ``?page=N`` to walk
            further pages).  The response is the standard DRF envelope:
            ``{count, next, previous, results}``.  See GH-463 — bulk staff-minted
            codes will eventually outgrow a single page and the project rule is
            "never remove backend pagination."

        POST
            Staff-only.  Body ``{"client": "<uid>"|null}`` -- a null/missing
            ``client`` mints a staff-bulk code that any sign-up can redeem.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
      tags:
        - Account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedInviteCodeList'
          description: ''
    post:
      operationId: account_invite_codes_create
      description: |-
        List or mint invite codes.

        GET
            Returns the codes attributed to the caller's accessible clients.
            Staff users may pass ``?client=<uid>`` to scope to one specific
            Client; without the filter, staff see codes for *every* client
            they can access (and superusers see them all).

            Paginated with the project default :class:`StandardPageNumberPagination`
            (20 per page, ``?page_size=`` capped at 100, ``?page=N`` to walk
            further pages).  The response is the standard DRF envelope:
            ``{count, next, previous, results}``.  See GH-463 — bulk staff-minted
            codes will eventually outgrow a single page and the project rule is
            "never remove backend pagination."

        POST
            Staff-only.  Body ``{"client": "<uid>"|null}`` -- a null/missing
            ``client`` mints a staff-bulk code that any sign-up can redeem.
      tags:
        - Account
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InviteCodeCreateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/InviteCodeCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/InviteCodeCreateRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InviteCodeMintResponse'
          description: ''
  /api/v1/account/invite-codes/{code}/revoke/:
    post:
      operationId: account_invite_codes_revoke_create
      description: |-
        Revoke an invite code.

        Members of the issuer Client (or staff) may revoke a code.  Returns
        409 if the code is already revoked or already redeemed.  Codes are
        looked up case-insensitively to match the redeem flow.
      parameters:
        - in: path
          name: code
          schema:
            type: string
          required: true
      tags:
        - Account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/account/invite-codes/validate/:
    get:
      operationId: account_invite_codes_validate_retrieve
      description: |-
        Public invite-code validation -- no auth required.

        Used by the sign-up form's pre-submit check (escape#127 / api#440).
        The response shape is intentionally minimal -- we never leak the
        issuer Client, the creator user, or the expiration date, just a
        boolean and a coarse error code so the form can render the right
        message.
      tags:
        - Account
      security:
        - bearerAuth: []
        - oauth2: []
        - {}
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InviteCodeValidateResponse'
          description: ''
  /api/v1/account/magic-link/request/:
    post:
      operationId: account_magic_link_request_create
      description: |-
        Request a magic link for authentication.

        POST /api/v1/account/magic-link/request/
        Body: { "email": "user@example.com" }

        Always returns 200 regardless of whether the email exists (no oracle).
      tags:
        - account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
        - {}
      responses:
        '200':
          description: No response body
  /api/v1/account/me/:
    get:
      operationId: account_me_retrieve
      description: |-
        Return the authenticated user's own profile.

        The new scope taxonomy (api#124) does not yet name a per-user
        "profile" scope, so this view continues to honour the legacy
        ``profile:read`` (or umbrella ``read``) catalog entries via
        :class:`HasScope`'s ``any_scope=True`` mode.  Session / JWT auth
        bypasses the scope check; third-party OAuth tokens carrying neither
        scope receive 403 — which is the contract callers from before
        GH-387 were already coded against.
      tags:
        - Account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
          description: ''
  /api/v1/account/me/first-run-tour/complete/:
    post:
      operationId: account_me_first_run_tour_complete_create
      description: |-
        Mark the in-app first-run guided tour as completed for the caller.

        ``POST /api/v1/account/me/first-run-tour/complete/`` (GH-661).

        Stamps :attr:`User.first_run_tour_completed_at` to ``now()`` the
        first time it's called for an account.  Subsequent calls return 200
        with the existing timestamp so the client can retry safely across
        flaky networks or two browser tabs racing the completion callback
        -- the column is a monotonic latch and never gets re-armed by this
        endpoint.

        Scoped implicitly to ``request.user``: there is no path parameter
        and the queryset is never widened, so a session for user A cannot
        flip the latch on user B's account.

        Audits ``user.first_run_tour_completed`` (category ``auth``) with
        ``metadata.already_completed`` set so operators can distinguish a
        genuine first-time completion from a redundant retry without
        pulling the row.
      tags:
        - Account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FirstRunTourCompleteResponse'
          description: ''
  /api/v1/account/me/preferences/:
    get:
      operationId: account_me_preferences_retrieve
      description: >-
        Read or write the authenticated user's preferences for the **current
        OAuth2 application** (the one the access token was issued to).  The
        ``data`` blob is opaque to the API -- callers own its schema.  For
        cross-application preferences such as ``theme``, use
        ``/me/preferences/global/`` instead.
      tags:
        - Account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Preference'
              examples:
                Per-appPreferences:
                  value:
                    id: 11111111-1111-4111-8111-111111111111
                    data:
                      sidebar_collapsed: true
                      default_view: schedule
                    created: '2026-04-25T14:30:00Z'
                    modified: '2026-04-25T14:30:00Z'
                  summary: GET /me/preferences/ -- arbitrary app-defined keys
                  description: >-
                    Per-OAuth2-application preferences are an opaque JSON blob
                    from the API's perspective.  Schema and key conventions are
                    owned by the calling application; ``theme`` lives on the
                    **global** row, not here.
          description: ''
    put:
      operationId: account_me_preferences_update
      description: >-
        Read or write the authenticated user's preferences for the **current
        OAuth2 application** (the one the access token was issued to).  The
        ``data`` blob is opaque to the API -- callers own its schema.  For
        cross-application preferences such as ``theme``, use
        ``/me/preferences/global/`` instead.
      tags:
        - Account
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreferenceRequest'
            examples:
              ReplacePer-appPreferences:
                value:
                  data:
                    sidebar_collapsed: true
                    default_view: schedule
                summary: PUT /me/preferences/ -- overwrite the data blob
                description: >-
                  PUT replaces the entire ``data`` object for the current OAuth2
                  application.
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PreferenceRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PreferenceRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Preference'
          description: ''
    patch:
      operationId: account_me_preferences_partial_update
      description: >-
        Read or write the authenticated user's preferences for the **current
        OAuth2 application** (the one the access token was issued to).  The
        ``data`` blob is opaque to the API -- callers own its schema.  For
        cross-application preferences such as ``theme``, use
        ``/me/preferences/global/`` instead.
      tags:
        - Account
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPreferenceRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPreferenceRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPreferenceRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Preference'
          description: ''
    delete:
      operationId: account_me_preferences_destroy
      description: >-
        Read or write the authenticated user's preferences for the **current
        OAuth2 application** (the one the access token was issued to).  The
        ``data`` blob is opaque to the API -- callers own its schema.  For
        cross-application preferences such as ``theme``, use
        ``/me/preferences/global/`` instead.
      tags:
        - Account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
  /api/v1/account/me/preferences/global/:
    get:
      operationId: account_me_preferences_global_retrieve
      description: >-
        Read or write the authenticated user's **global** preferences row
        (``application=NULL``).  This row holds cross-application preferences
        such as the user's theme.


        Documented keys on ``data``:


        * ``theme`` -- one of ``light``, ``dark``, or ``system``.  Any other
        value (including missing) is treated as ``system`` when the value is
        read back, so the OS-level ``prefers-color-scheme`` setting wins.  The
        web app and the Security Nexus both read and write this same key so a
        single user-facing toggle controls both surfaces.


        Additional keys may be added by first-party clients over time and are
        preserved verbatim by PATCH; PUT replaces the object wholesale.
      tags:
        - Account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Preference'
              examples:
                GlobalPreferencesWithThemeSet:
                  value:
                    id: 11111111-1111-4111-8111-111111111111
                    data:
                      theme: dark
                    created: '2026-04-25T14:30:00Z'
                    modified: '2026-04-25T14:30:00Z'
                  summary: GET /me/preferences/global/ -- user has chosen dark mode
                  description: >-
                    Typical response for a user who has selected a non-default
                    theme. The web app and the Security Nexus both read this row
                    to render in the requested mode.
          description: ''
    put:
      operationId: account_me_preferences_global_update
      description: >-
        Read or write the authenticated user's **global** preferences row
        (``application=NULL``).  This row holds cross-application preferences
        such as the user's theme.


        Documented keys on ``data``:


        * ``theme`` -- one of ``light``, ``dark``, or ``system``.  Any other
        value (including missing) is treated as ``system`` when the value is
        read back, so the OS-level ``prefers-color-scheme`` setting wins.  The
        web app and the Security Nexus both read and write this same key so a
        single user-facing toggle controls both surfaces.


        Additional keys may be added by first-party clients over time and are
        preserved verbatim by PATCH; PUT replaces the object wholesale.
      tags:
        - Account
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreferenceRequest'
            examples:
              SetThemeToDark:
                value:
                  data:
                    theme: dark
                summary: 'PUT /me/preferences/global/ -- replace data with {theme: dark}'
                description: >-
                  PUT replaces the entire ``data`` object.  Allowed values for
                  ``theme`` are ``light``, ``dark``, and ``system``; any other
                  value is treated as ``system`` when the theme is read back by
                  the Nexus or the web app.
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PreferenceRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PreferenceRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Preference'
              examples:
                ThemePersisted:
                  value:
                    id: 11111111-1111-4111-8111-111111111111
                    data:
                      theme: dark
                    created: '2026-04-25T14:30:00Z'
                    modified: '2026-04-25T14:31:12Z'
                  summary: 200 OK -- write succeeded
                  description: >-
                    Returned after a successful PUT or PATCH that updated the
                    theme.
          description: ''
    patch:
      operationId: account_me_preferences_global_partial_update
      description: >-
        Read or write the authenticated user's **global** preferences row
        (``application=NULL``).  This row holds cross-application preferences
        such as the user's theme.


        Documented keys on ``data``:


        * ``theme`` -- one of ``light``, ``dark``, or ``system``.  Any other
        value (including missing) is treated as ``system`` when the value is
        read back, so the OS-level ``prefers-color-scheme`` setting wins.  The
        web app and the Security Nexus both read and write this same key so a
        single user-facing toggle controls both surfaces.


        Additional keys may be added by first-party clients over time and are
        preserved verbatim by PATCH; PUT replaces the object wholesale.
      tags:
        - Account
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPreferenceRequest'
            examples:
              SwitchThemeWithoutDisturbingOtherKeys:
                value:
                  data:
                    theme: light
                summary: 'PATCH /me/preferences/global/ -- shallow-merge {theme: light}'
                description: >-
                  PATCH shallow-merges into the existing ``data`` object, so
                  other keys (locale, timezone, ...) on the global row are
                  preserved.  Allowed values for ``theme`` are ``light``,
                  ``dark``, and ``system``.
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPreferenceRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPreferenceRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Preference'
              examples:
                ThemePersisted:
                  value:
                    id: 11111111-1111-4111-8111-111111111111
                    data:
                      theme: dark
                    created: '2026-04-25T14:30:00Z'
                    modified: '2026-04-25T14:31:12Z'
                  summary: 200 OK -- write succeeded
                  description: >-
                    Returned after a successful PUT or PATCH that updated the
                    theme.
          description: ''
  /api/v1/account/passkey/:
    get:
      operationId: account_passkey_retrieve
      description: |-
        List the authenticated user's registered passkeys.

        GET /api/v1/account/passkey/
      tags:
        - account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/account/passkey/{passkey_uid}/:
    delete:
      operationId: account_passkey_destroy
      description: >-
        Delete a registered passkey.


        DELETE /api/v1/account/passkey/<pk>/


        Returns 400 if this is the user's only passkey (last-passkey
        protection).
      parameters:
        - in: path
          name: passkey_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
  /api/v1/account/passkey/{passkey_uid}/rename/:
    patch:
      operationId: account_passkey_rename_partial_update
      description: |-
        Rename a registered passkey.

        PATCH /api/v1/account/passkey/<pk>/rename/
        Body: { "name": "New Name" }
      parameters:
        - in: path
          name: passkey_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/account/passkey/auth/begin/:
    post:
      operationId: account_passkey_auth_begin_create
      description: |-
        Begin passkey authentication ceremony.

        POST /api/v1/account/passkey/auth/begin/
        Body: { "email": "user@example.com" }  (optional)

        If email is provided, returns allowCredentials scoped to that user.
        If email is omitted, returns empty allowCredentials for discoverable
        credential / conditional UI flow.
      tags:
        - account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
        - {}
      responses:
        '200':
          description: No response body
  /api/v1/account/passkey/auth/complete/:
    post:
      operationId: account_passkey_auth_complete_create
      description: |-
        Complete passkey authentication ceremony.

        POST /api/v1/account/passkey/auth/complete/
        Body: { "assertion": {...}, "challenge_key": "..." }

        On success, returns JWT access + refresh tokens.
      tags:
        - account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
        - {}
      responses:
        '200':
          description: No response body
  /api/v1/account/passkey/register/begin/:
    post:
      operationId: account_passkey_register_begin_create
      description: |-
        Begin passkey registration ceremony.

        POST /api/v1/account/passkey/register/begin/
        Auth: Bearer token (setup or full session)
      tags:
        - account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/account/passkey/register/complete/:
    post:
      operationId: account_passkey_register_complete_create
      description: |-
        Complete passkey registration ceremony.

        POST /api/v1/account/passkey/register/complete/
        Auth: Bearer token (setup or full session)
        Body: { "credential": {...}, "name": "My Passkey" }

        On success during setup session: auto-upgrades to full session.
      tags:
        - account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/account/signup/:
    post:
      operationId: account_signup_create
      description: |-
        Public self-service signup with optional invite-code gating (GH-440).

        POST /api/v1/account/signup/
        Body::

            {
                "email": "owner@newpractice.test",
                "first_name": "Pat",
                "last_name": "Ient",
                "client_name": "New Practice DSO",
                "invite_code": "ABCDEFGH2345"   // optional unless flag is on
            }

        Behaviour
        ---------
        * Always :class:`AllowAny` -- this is the entry point into the
          product for new tenants.  Throttled by the project-wide ``anon``
          scope so it can't be turned into a Client-creation cannon.

        * When the ``invited_signup_required`` Waffle flag is **off** for
          the request, ``invite_code`` is ignored -- a missing or invalid
          code does not block signup.  This is the day-one default.

        * When the flag is **on**, ``invite_code`` is required and is
          atomically redeemed inside the same transaction that creates the
          User, Client, and ClientMembership.  The redemption uses
          ``select_for_update`` (see :meth:`InviteCode.redeem`) so two
          simultaneous signups racing for the same code can't both win --
          one gets the new account, the other gets a 400.

        Atomic boundary
        ---------------
        Everything happens inside a single ``transaction.atomic`` block:

        1. Create the new :class:`apps.practices.models.Client`.
        2. Create the new :class:`User` (passwordless -- the next step in
           the signup wizard is magic-link → passkey enrolment).
        3. Create the :class:`ClientMembership` with role ``OWNER``.
        4. (When the flag is on) atomically redeem the invite code.

        If step 4 raises -- e.g. someone revoked the code between
        pre-flight validation and the POST landing -- the whole
        transaction rolls back: no orphan Client, no orphan User, no
        leaked email collision.
      tags:
        - Account
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SignupRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/SignupRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/SignupRequest'
        required: true
      security:
        - bearerAuth: []
        - oauth2: []
        - {}
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignupResponse'
          description: ''
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignupInviteCodeError'
          description: >-
            Invite-code validation failed -- only emitted when the
            ``invited_signup_required`` Waffle flag is active.  Body shape is
            ``{"invite_code": ["..."]}``.
  /api/v1/account/token/logout/:
    post:
      operationId: account_token_logout_create
      description: |-
        Revoke tokens and flush all Django sessions for the user.

        Accepts a refresh token from any of:

        * The HttpOnly ``escape_refresh_token`` cookie (preferred — set by
          the API on login, cleared on logout, never visible to JS).
        * A SimpleJWT refresh token in the body (``{"refresh": "..."}``)
          for backward compatibility with clients that have not yet
          switched to the cookie flow.
        * An OAuth2 access token via the ``Authorization`` header (older
          script tooling).

        In every case, all Django sessions for the identified user are
        flushed so the Nexus session cookie is invalidated, and the
        refresh-token cookie is unset on the response.
      tags:
        - Account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
        - {}
      responses:
        '200':
          description: No response body
  /api/v1/account/token/refresh/:
    post:
      operationId: account_token_refresh_create
      description: |-
        Cookie-aware wrapper around SimpleJWT's :class:`TokenRefreshView`.

        Behavioural differences from the base class:

        * If the request body has no ``refresh`` field, the view falls
          back to reading the ``escape_refresh_token`` HttpOnly cookie
          and forwards it to the serializer.  This is the common path
          for the web SPA after GH-293.
        * On a successful 200 response, the rotated refresh token is
          written to the cookie and stripped from the response body so
          no JS-readable copy is ever returned to the browser.  The
          access token stays in the body where the SPA can stash it in
          memory (or component state) for the short access-token TTL.
        * Body-based callers (curl, integration tests, legacy clients)
          that POST a ``refresh`` field still see the rotated refresh
          token in the response so they keep working during the
          rollout window.
        * The serializer is overridden to
          :class:`apps.accounts.jwt_tokens.ChainCappedTokenRefreshSerializer`
          (GH-564) so the rotation chain inherits an absolute lifetime
          cap.  An active polling client can no longer keep refreshing
          forever just by exchanging tokens within the per-token
          lifetime — the chain dies once the configured absolute
          window elapses, anchored to the *original* login.
      tags:
        - Account
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChainCappedTokenRefreshRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ChainCappedTokenRefreshRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ChainCappedTokenRefreshRequest'
        required: true
      security:
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChainCappedTokenRefresh'
          description: ''
  /api/v1/account/token/verify/:
    post:
      operationId: account_token_verify_create
      description: |-
        Takes a token and indicates if it is valid.  This view provides no
        information about a token's fitness for a particular use.
      tags:
        - account
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TokenVerifyRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/TokenVerifyRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/TokenVerifyRequest'
        required: true
      security:
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/account/user/:
    get:
      operationId: account_user_list
      description: >-
        List users visible to the authenticated user (scoped by practice
        membership).


        Optional boolean filters (``?is_staff=true|false`` and

        ``?is_superuser=true|false``) let callers narrow the result to the

        staff / superuser populations (or exclude them). Both filters accept

        the strings ``"true"`` and ``"false"`` (case-insensitive); any other

        value is ignored so unknown params never surface as a 500.
      parameters:
        - name: cursor
          required: false
          in: query
          description: The pagination cursor value.
          schema:
            type: string
      tags:
        - account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedUserList'
          description: ''
  /api/v1/account/user/{user_uid}/:
    get:
      operationId: account_user_retrieve
      description: |-
        Retrieve or update a user by ID (scoped to client).

        Reads are allowed for any authenticated user within the same client.
        Writes are restricted: users can update their own profile, but only
        admins can update other users' profiles.
      parameters:
        - in: path
          name: user_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
          description: ''
    put:
      operationId: account_user_update
      description: |-
        Retrieve or update a user by ID (scoped to client).

        Reads are allowed for any authenticated user within the same client.
        Writes are restricted: users can update their own profile, but only
        admins can update other users' profiles.
      parameters:
        - in: path
          name: user_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - account
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/UserRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/UserRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
          description: ''
    patch:
      operationId: account_user_partial_update
      description: |-
        Retrieve or update a user by ID (scoped to client).

        Reads are allowed for any authenticated user within the same client.
        Writes are restricted: users can update their own profile, but only
        admins can update other users' profiles.
      parameters:
        - in: path
          name: user_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - account
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedUserRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedUserRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedUserRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
          description: ''
  /api/v1/admin/api-stats/:
    get:
      operationId: admin_api_stats_list
      description: >-
        Aggregate the OAuthAppUsageMetric rollup table by scope, method, and
        window. Staff-only. Powers the support-tool API stats dashboard.
      summary: Read OAuth API usage rollups
      parameters:
        - in: query
          name: from
          schema:
            type: string
            format: date
          description: Inclusive lower bound on bucket_start (UTC date, YYYY-MM-DD).
        - in: query
          name: method
          schema:
            type: string
            enum:
              - DELETE
              - GET
              - HEAD
              - OPTIONS
              - PATCH
              - POST
              - PUT
          description: >-
            HTTP method. GET/HEAD/OPTIONS map to the GET bucket;
            POST/PUT/PATCH/DELETE map to the WRITE bucket.
        - in: query
          name: page
          schema:
            type: integer
          description: 1-indexed page number.
        - in: query
          name: page_size
          schema:
            type: integer
          description: Page size, default 20, max 100.
        - in: query
          name: scope
          schema:
            type: string
            enum:
              - app
              - client
              - practice
              - user
          description: Scope dimension to filter on.
        - in: query
          name: scope_id
          schema:
            type: string
          description: >-
            Denormalized scope identifier. UUID for client/practice/user, OAuth
            client_id for app. Requires `scope` to be set.
        - in: query
          name: to
          schema:
            type: string
            format: date
          description: Inclusive upper bound on bucket_start (UTC date, YYYY-MM-DD).
        - in: query
          name: window
          schema:
            type: string
            enum:
              - day
              - hour
              - month
              - week
          description: Bucket size. Defaults to hour.
      tags:
        - Admin
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedApiStatsRowList'
          description: ''
  /api/v1/admin/audit/events/:
    get:
      operationId: admin_audit_events_list
      description: >-
        Paginated, filterable read of the ``audit-events-*`` Elasticsearch
        index. Staff-only. Powers the support-tool audit-log panel. Filters are
        AND-combined; missing filters are open-ended. Page size defaults to 50,
        max 100.
      summary: List audit events
      parameters:
        - in: query
          name: actor_email
          schema:
            type: string
          description: >-
            Exact-match actor email; matched against the keyword field
            ``actor.email``.
        - in: query
          name: event_type
          schema:
            type: string
          description: Dotted-name event type (e.g. ``patient.viewed``).
        - in: query
          name: page
          schema:
            type: integer
          description: 1-indexed page number. Defaults to 1.
        - in: query
          name: page_size
          schema:
            type: integer
          description: Page size, default 50, max 100 (clamped).
        - in: query
          name: practice
          schema:
            type: string
            format: uuid
          description: >-
            Practice UUID — only events tagged with this practice_id are
            returned.
        - in: query
          name: since
          schema:
            type: string
            format: date-time
          description: ISO8601 lower bound (inclusive) on ``timestamp``.
        - in: query
          name: until
          schema:
            type: string
            format: date-time
          description: ISO8601 upper bound (inclusive) on ``timestamp``.
      tags:
        - Admin
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AdminAuditEventRow'
          description: ''
  /api/v1/admin/client/{id}/:
    patch:
      operationId: admin_client_partial_update
      description: |-
        Toggle ``is_active`` on a Client.

        Staff-only (``is_staff=True``). Sets the ``is_active`` flag on the
        Client model. Deactivating a client effectively disables all practices
        under it.

        Idempotent: re-submitting the current state is a no-op that still
        writes an audit entry (with ``no_op=True``) so the attempt itself is
        recorded.
      parameters:
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Admin
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedAdminToggleActiveRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedAdminToggleActiveRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedAdminToggleActiveRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminClient'
          description: ''
  /api/v1/admin/magic-link/:
    post:
      operationId: admin_magic_link_create
      description: |-
        Generate a magic link for any user.

        Staff-only endpoint. Creates a time-limited, single-use magic link
        that can be handed to a user out-of-band for passkey recovery.
        Reuses the same infrastructure as the ``create_magic_link`` management
        command.
      tags:
        - Admin
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminMagicLinkRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AdminMagicLinkRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/AdminMagicLinkRequestRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminMagicLinkResponse'
          description: ''
  /api/v1/admin/mcp/tools/:
    get:
      operationId: admin_mcp_tools_list
      description: >-
        Read-only list of every key registered in
        apps.mcp.tools.registry.REGISTRY. Staff-only. Returns name, description,
        required_scopes, and pass_context. Used by the support UI to render
        every gating row even when no PracticeToolGate exists yet for the
        practice in view.
      summary: List the canonical MCP tool catalog
      tags:
        - Admin
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AdminMCPToolCatalog'
          description: ''
  /api/v1/admin/oauth-app/:
    get:
      operationId: admin_oauth_app_list
      description: |-
        Paginated list of OAuth applications for the support tool.

        Cross-tenant — staff operates above the practice-scoping layer,
        so every Application row is returned.  Ordered by ``name`` for a
        stable, human-friendly default; the support UI may later re-sort
        client-side.

        Query parameters (api#408):

        * ``q`` — case-insensitive substring match against ``name`` OR
          ``client_id``.  Empty/missing means "no filter".  Translates to
          ``Q(name__icontains=q) | Q(client_id__icontains=q)`` so the index
          planner picks the cheaper side.
        * ``throttle_mode`` — exact match against
          :class:`OAuthAppProfile.throttle_mode` (``default`` / ``audit`` /
          ``enforce``).  Apps with NO profile row are treated as
          ``default`` so ``?throttle_mode=default`` returns them too —
          implemented with a ``LEFT JOIN`` + ``Coalesce`` annotation rather
          than a Python-side filter so pagination metadata
          (``count``/``next``/``previous``) reflects the filtered set.

        Both filters AND-combine.  An unknown ``throttle_mode`` value
        returns **HTTP 400** with a clear error — picked over silently
        ignoring because a typo on the support tool's side should fail
        loudly rather than serve up the unfiltered list and look like a
        pagination bug.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
      tags:
        - admin
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedAdminOAuthAppList'
          description: ''
  /api/v1/admin/oauth-app/{client_id}/:
    get:
      operationId: admin_oauth_app_retrieve
      description: Return the public payload for a single OAuth application.
      summary: Get OAuth application detail
      parameters:
        - in: path
          name: client_id
          schema:
            type: string
          required: true
      tags:
        - Admin
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOAuthApp'
          description: ''
    patch:
      operationId: admin_oauth_app_partial_update
      description: >-
        Either flips the per-app throttle override (``throttle_mode`` +
        ``reason`` body) or updates the slot manifest (``slots`` /
        ``url_template`` body) — or both in a single request.  At least one set
        of fields must be present. Staff-only.
      summary: Update OAuth application throttle mode and / or slot manifest
      parameters:
        - in: path
          name: client_id
          schema:
            type: string
          required: true
      tags:
        - Admin
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedAdminOAuthAppThrottleModeRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedAdminOAuthAppThrottleModeRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedAdminOAuthAppThrottleModeRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOAuthApp'
          description: ''
  /api/v1/admin/oauth-app/{client_id}/approve/:
    post:
      operationId: admin_oauth_app_approve_create
      description: |-
        Approve, reject, or suspend a third-party OAuth application.

        ``POST /api/v1/admin/oauth-app/<client_id>/approve/``
        ``POST /api/v1/admin/oauth-app/<client_id>/reject/``
        ``POST /api/v1/admin/oauth-app/<client_id>/suspend/``

        The ``action`` kwarg is set by the URL dispatcher (see
        ``admin_api/urls.py``).

        Approve:
            Sets ``approval_status=APPROVED``, stamps ``approved_at`` and
            ``approved_by``.  ``reason`` is optional (but logged if supplied).
            Returns 200 with the updated serializer payload.

        Reject / Suspend:
            Sets the corresponding status.  ``reason`` is required (≥5 chars)
            so there is always a human-readable explanation in the audit log.
            ``approved_at`` / ``approved_by`` are cleared on reject/suspend so
            the record accurately reflects the current state.

        First-party apps:
            Always returns 400 — they bypass the approval workflow by design
            and must not be touched via this endpoint.
      parameters:
        - in: path
          name: client_id
          schema:
            type: string
          required: true
      tags:
        - admin
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/admin/oauth-app/{client_id}/reject/:
    post:
      operationId: admin_oauth_app_reject_create
      description: |-
        Approve, reject, or suspend a third-party OAuth application.

        ``POST /api/v1/admin/oauth-app/<client_id>/approve/``
        ``POST /api/v1/admin/oauth-app/<client_id>/reject/``
        ``POST /api/v1/admin/oauth-app/<client_id>/suspend/``

        The ``action`` kwarg is set by the URL dispatcher (see
        ``admin_api/urls.py``).

        Approve:
            Sets ``approval_status=APPROVED``, stamps ``approved_at`` and
            ``approved_by``.  ``reason`` is optional (but logged if supplied).
            Returns 200 with the updated serializer payload.

        Reject / Suspend:
            Sets the corresponding status.  ``reason`` is required (≥5 chars)
            so there is always a human-readable explanation in the audit log.
            ``approved_at`` / ``approved_by`` are cleared on reject/suspend so
            the record accurately reflects the current state.

        First-party apps:
            Always returns 400 — they bypass the approval workflow by design
            and must not be touched via this endpoint.
      parameters:
        - in: path
          name: client_id
          schema:
            type: string
          required: true
      tags:
        - admin
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/admin/oauth-app/{client_id}/suspend/:
    post:
      operationId: admin_oauth_app_suspend_create
      description: |-
        Approve, reject, or suspend a third-party OAuth application.

        ``POST /api/v1/admin/oauth-app/<client_id>/approve/``
        ``POST /api/v1/admin/oauth-app/<client_id>/reject/``
        ``POST /api/v1/admin/oauth-app/<client_id>/suspend/``

        The ``action`` kwarg is set by the URL dispatcher (see
        ``admin_api/urls.py``).

        Approve:
            Sets ``approval_status=APPROVED``, stamps ``approved_at`` and
            ``approved_by``.  ``reason`` is optional (but logged if supplied).
            Returns 200 with the updated serializer payload.

        Reject / Suspend:
            Sets the corresponding status.  ``reason`` is required (≥5 chars)
            so there is always a human-readable explanation in the audit log.
            ``approved_at`` / ``approved_by`` are cleared on reject/suspend so
            the record accurately reflects the current state.

        First-party apps:
            Always returns 400 — they bypass the approval workflow by design
            and must not be touched via this endpoint.
      parameters:
        - in: path
          name: client_id
          schema:
            type: string
          required: true
      tags:
        - admin
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/admin/practice/{id}/:
    patch:
      operationId: admin_practice_partial_update
      description: |-
        Toggle ``is_active`` on a Practice.

        Staff-only (``is_staff=True``). Sets the ``is_active`` flag on the
        Practice model.

        Idempotent: re-submitting the current state is a no-op that still
        writes an audit entry (with ``no_op=True``).
      parameters:
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Admin
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedAdminToggleActiveRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedAdminToggleActiveRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedAdminToggleActiveRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminPractice'
          description: ''
  /api/v1/admin/practice/{id}/ocr/backfill/:
    post:
      operationId: admin_practice_ocr_backfill_create
      description: >-
        Queue OCR + Elasticsearch indexing for documents in the given practice
        that pre-date the OCR pipeline. Idempotent: skips documents that already
        have a DocumentOcrResult row unless `force` is true. Requires `reason`,
        which is recorded in the audit log.
      parameters:
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Admin
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OcrBackfillRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OcrBackfillRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/OcrBackfillRequestRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OcrBackfillResponse'
          description: ''
  /api/v1/admin/practice/{id}/ocr/status/:
    get:
      operationId: admin_practice_ocr_status_retrieve
      description: >-
        Return `{documents_total, ocr_results_total, pending}` for the practice.
        Lets the support UI render OCR progress without paginating either list.
      parameters:
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Admin
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OcrStatusResponse'
          description: ''
  /api/v1/admin/practice/{practice_id}/mcp/gates/:
    get:
      operationId: admin_practice_mcp_gates_list
      description: |-
        List existing :class:`PracticeToolGate` rows for one practice.

        Empty list means "no overrides -- every tool is default-enabled".
        Returns the rows in ``tool_key`` order so the support UI's
        rendering is stable across requests.
      parameters:
        - in: path
          name: practice_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - admin
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AdminPracticeToolGate'
          description: ''
  /api/v1/admin/practice/{practice_id}/mcp/gates/{tool_key}/:
    put:
      operationId: admin_practice_mcp_gates_update
      description: >-
        Create or update a PracticeToolGate row.  Fires the model's post_save
        signal so connected MCP SSE clients receive a
        notifications/tools/list_changed frame.
      summary: Upsert an MCP tool gate for a practice
      parameters:
        - in: path
          name: practice_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: tool_key
          schema:
            type: string
          required: true
      tags:
        - Admin
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminPracticeToolGateUpsertRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AdminPracticeToolGateUpsertRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/AdminPracticeToolGateUpsertRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminPracticeToolGate'
          description: ''
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminPracticeToolGate'
          description: ''
    delete:
      operationId: admin_practice_mcp_gates_destroy
      description: >-
        Delete the PracticeToolGate row, reverting the tool to its
        default-enabled state.  Fires the model's post_delete signal so
        connected MCP SSE clients receive a notifications/tools/list_changed
        frame.  Idempotent -- deleting a non-existent gate returns 404.
      summary: Remove an MCP tool gate for a practice
      parameters:
        - in: path
          name: practice_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: tool_key
          schema:
            type: string
          required: true
      tags:
        - Admin
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '404':
          description: No response body
  /api/v1/admin/support/patients/:
    get:
      operationId: admin_support_patients_list
      description: |-
        Cross-practice, PHI-free patient search for the support tool.

        Staff-only (``is_staff=True``).  Accepts a ``?q=<query>`` parameter:

        * If ``q`` is UUID-shaped, does an exact match on ``Patient.id``.
        * Otherwise, performs a case-insensitive prefix match against
          ``first_name`` OR ``last_name``.

        The PHI fields are searched server-side -- a tech needs to type a
        name to find a record -- but the *response* never contains them.
        The query string itself is NOT recorded in the audit log, because
        it may itself be PHI (e.g. a patient's surname).
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
      tags:
        - admin
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedSupportPatientList'
          description: ''
  /api/v1/admin/support/patients/{id}/:
    get:
      operationId: admin_support_patients_retrieve
      description: |-
        PHI-free patient detail view for the support tool.

        Staff-only (``is_staff=True``).  Exposes the same allow-listed
        fields as the list view (see :class:`SupportPatientSerializer`).
        Emits a ``support.patient.viewed`` audit event on every access.
      parameters:
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - admin
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportPatient'
          description: ''
  /api/v1/admin/user/{id}/:
    patch:
      operationId: admin_user_partial_update
      description: |-
        Toggle ``is_active`` on a User.

        Staff-only (``is_staff=True``). Sets the ``is_active`` flag on the
        User model.

        Only superusers may disable a superuser -- a non-superuser staff
        member attempting this gets a 403.  The support tool enforces the
        same rule client-side; the server check is the authoritative
        guard (belt + suspenders against a broken or misbehaving client).

        Idempotent: re-submitting the current state is a no-op that still
        writes an audit entry (with ``no_op=True``).
      parameters:
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Admin
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedAdminToggleActiveRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedAdminToggleActiveRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedAdminToggleActiveRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminUser'
          description: ''
  /api/v1/banner/:
    get:
      operationId: banner_list
      description: |-
        CRUD for sign-in / pop-up banners attached to a Client or a Practice.

        **Permissions (deny-by-default):**

        - All requests require an authenticated, non-patient staff user.
        - **Read** (list/retrieve): any user with an active membership in the
          banner's owning Client (for client-scoped banners) or Practice
          (for practice-scoped banners).  By default the list returns only
          currently-active banners; pass ``?include_expired=true`` to include
          expired and not-yet-started banners (admin role required).
        - **Write** (create/update/destroy):
            * Banners targeting a Client require an active ClientMembership
              on that Client with role OWNER or TECHNICAL.
            * Banners targeting a Practice require either:
                - an active PracticeMembership on that Practice with role
                  ADMIN or OFFICE_MANAGER, **or**
                - an active ClientMembership on the Practice's Client with
                  role OWNER (Owners can manage banners on any of their
                  practices).
            * Crucially, a Practice admin **cannot** create Client-level
              banners — that requires explicit Client admin rights.

        Cross-tenant safety: ``get_queryset`` always restricts to the user's
        accessible clients and practices, so a user in tenant A cannot see,
        list, or PATCH a banner in tenant B even by guessing UUIDs.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
      tags:
        - Banners
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedBannerList'
          description: ''
    post:
      operationId: banner_create
      description: |-
        CRUD for sign-in / pop-up banners attached to a Client or a Practice.

        **Permissions (deny-by-default):**

        - All requests require an authenticated, non-patient staff user.
        - **Read** (list/retrieve): any user with an active membership in the
          banner's owning Client (for client-scoped banners) or Practice
          (for practice-scoped banners).  By default the list returns only
          currently-active banners; pass ``?include_expired=true`` to include
          expired and not-yet-started banners (admin role required).
        - **Write** (create/update/destroy):
            * Banners targeting a Client require an active ClientMembership
              on that Client with role OWNER or TECHNICAL.
            * Banners targeting a Practice require either:
                - an active PracticeMembership on that Practice with role
                  ADMIN or OFFICE_MANAGER, **or**
                - an active ClientMembership on the Practice's Client with
                  role OWNER (Owners can manage banners on any of their
                  practices).
            * Crucially, a Practice admin **cannot** create Client-level
              banners — that requires explicit Client admin rights.

        Cross-tenant safety: ``get_queryset`` always restricts to the user's
        accessible clients and practices, so a user in tenant A cannot see,
        list, or PATCH a banner in tenant B even by guessing UUIDs.
      tags:
        - Banners
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BannerRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/BannerRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/BannerRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Banner'
          description: ''
  /api/v1/banner/{id}/:
    get:
      operationId: banner_retrieve
      description: |-
        CRUD for sign-in / pop-up banners attached to a Client or a Practice.

        **Permissions (deny-by-default):**

        - All requests require an authenticated, non-patient staff user.
        - **Read** (list/retrieve): any user with an active membership in the
          banner's owning Client (for client-scoped banners) or Practice
          (for practice-scoped banners).  By default the list returns only
          currently-active banners; pass ``?include_expired=true`` to include
          expired and not-yet-started banners (admin role required).
        - **Write** (create/update/destroy):
            * Banners targeting a Client require an active ClientMembership
              on that Client with role OWNER or TECHNICAL.
            * Banners targeting a Practice require either:
                - an active PracticeMembership on that Practice with role
                  ADMIN or OFFICE_MANAGER, **or**
                - an active ClientMembership on the Practice's Client with
                  role OWNER (Owners can manage banners on any of their
                  practices).
            * Crucially, a Practice admin **cannot** create Client-level
              banners — that requires explicit Client admin rights.

        Cross-tenant safety: ``get_queryset`` always restricts to the user's
        accessible clients and practices, so a user in tenant A cannot see,
        list, or PATCH a banner in tenant B even by guessing UUIDs.
      parameters:
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          description: A UUID string identifying this banner.
          required: true
      tags:
        - Banners
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Banner'
          description: ''
    put:
      operationId: banner_update
      description: |-
        CRUD for sign-in / pop-up banners attached to a Client or a Practice.

        **Permissions (deny-by-default):**

        - All requests require an authenticated, non-patient staff user.
        - **Read** (list/retrieve): any user with an active membership in the
          banner's owning Client (for client-scoped banners) or Practice
          (for practice-scoped banners).  By default the list returns only
          currently-active banners; pass ``?include_expired=true`` to include
          expired and not-yet-started banners (admin role required).
        - **Write** (create/update/destroy):
            * Banners targeting a Client require an active ClientMembership
              on that Client with role OWNER or TECHNICAL.
            * Banners targeting a Practice require either:
                - an active PracticeMembership on that Practice with role
                  ADMIN or OFFICE_MANAGER, **or**
                - an active ClientMembership on the Practice's Client with
                  role OWNER (Owners can manage banners on any of their
                  practices).
            * Crucially, a Practice admin **cannot** create Client-level
              banners — that requires explicit Client admin rights.

        Cross-tenant safety: ``get_queryset`` always restricts to the user's
        accessible clients and practices, so a user in tenant A cannot see,
        list, or PATCH a banner in tenant B even by guessing UUIDs.
      parameters:
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          description: A UUID string identifying this banner.
          required: true
      tags:
        - Banners
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BannerRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/BannerRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/BannerRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Banner'
          description: ''
    patch:
      operationId: banner_partial_update
      description: |-
        CRUD for sign-in / pop-up banners attached to a Client or a Practice.

        **Permissions (deny-by-default):**

        - All requests require an authenticated, non-patient staff user.
        - **Read** (list/retrieve): any user with an active membership in the
          banner's owning Client (for client-scoped banners) or Practice
          (for practice-scoped banners).  By default the list returns only
          currently-active banners; pass ``?include_expired=true`` to include
          expired and not-yet-started banners (admin role required).
        - **Write** (create/update/destroy):
            * Banners targeting a Client require an active ClientMembership
              on that Client with role OWNER or TECHNICAL.
            * Banners targeting a Practice require either:
                - an active PracticeMembership on that Practice with role
                  ADMIN or OFFICE_MANAGER, **or**
                - an active ClientMembership on the Practice's Client with
                  role OWNER (Owners can manage banners on any of their
                  practices).
            * Crucially, a Practice admin **cannot** create Client-level
              banners — that requires explicit Client admin rights.

        Cross-tenant safety: ``get_queryset`` always restricts to the user's
        accessible clients and practices, so a user in tenant A cannot see,
        list, or PATCH a banner in tenant B even by guessing UUIDs.
      parameters:
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          description: A UUID string identifying this banner.
          required: true
      tags:
        - Banners
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedBannerRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedBannerRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedBannerRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Banner'
          description: ''
    delete:
      operationId: banner_destroy
      description: |-
        CRUD for sign-in / pop-up banners attached to a Client or a Practice.

        **Permissions (deny-by-default):**

        - All requests require an authenticated, non-patient staff user.
        - **Read** (list/retrieve): any user with an active membership in the
          banner's owning Client (for client-scoped banners) or Practice
          (for practice-scoped banners).  By default the list returns only
          currently-active banners; pass ``?include_expired=true`` to include
          expired and not-yet-started banners (admin role required).
        - **Write** (create/update/destroy):
            * Banners targeting a Client require an active ClientMembership
              on that Client with role OWNER or TECHNICAL.
            * Banners targeting a Practice require either:
                - an active PracticeMembership on that Practice with role
                  ADMIN or OFFICE_MANAGER, **or**
                - an active ClientMembership on the Practice's Client with
                  role OWNER (Owners can manage banners on any of their
                  practices).
            * Crucially, a Practice admin **cannot** create Client-level
              banners — that requires explicit Client admin rights.

        Cross-tenant safety: ``get_queryset`` always restricts to the user's
        accessible clients and practices, so a user in tenant A cannot see,
        list, or PATCH a banner in tenant B even by guessing UUIDs.
      parameters:
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          description: A UUID string identifying this banner.
          required: true
      tags:
        - Banners
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
  /api/v1/client/:
    get:
      operationId: client_list
      description: |-
        List and create client (organization) records for the current user.

        ``GET  /api/v1/client/`` -- list the caller's accessible organizations
        (unchanged behaviour).

        ``POST /api/v1/client/`` -- create a new organization for the
        caller (api#469).  Used by the first-run ``/getting-started`` wizard
        when the user has no existing clients.  See :meth:`post` for the full
        contract.
      tags:
        - client
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Client'
          description: ''
    post:
      operationId: client_create
      description: |-
        Create a new organization (Client) for the authenticated staff user.

        Creates the :class:`Client` row, then mints an ``OWNER``
        :class:`~apps.accounts.models.ClientMembership` so the user
        immediately has full control.  New clients are automatically
        assigned the ``free`` plan by :meth:`Client.save`.

        Returns HTTP 201 with the new client payload on success.
        Returns HTTP 409 if the user already belongs to at least one
        active client — the getting-started wizard guards against this
        case on the frontend, but the API enforces it so the endpoint
        can't be used as an org-creation cannon.
      tags:
        - Practices
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: Organization name
              required:
                - name
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Client'
          description: ''
        '400':
          description: Missing or blank organization name.
        '409':
          description: The user already has an active organization.
  /api/v1/client/{client_uid}/billing/portal-session/:
    post:
      operationId: client_billing_portal_session_create
      description: |-
        Mint a Stripe-hosted billing portal session for the caller's Client.

        POST /api/v1/client/<client_uid>/billing/portal-session/

        The Stripe billing portal lets customers update their payment method,
        download past invoices, and cancel / resume their subscription.
        Letting Stripe host the surface is the standard self-service pattern
        and keeps PCI scope at SAQ-A.

        Permissions: the caller must hold a membership on the target Client
        (direct ``ClientMembership`` or via a ``PracticeMembership`` on one of
        the Client's practices).  Cross-tenant UID guesses get a 404 so we
        never leak Client existence.

        No PHI is sent to Stripe (the existing ``ensure_customer`` helper
        only ships the business name + ``client_uid`` metadata).  The signed
        portal URL is *not* audit-logged because it carries a single-use
        token; we record the create event with the Client + return_url only.
      parameters:
        - in: path
          name: client_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Billing
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BillingPortalSessionRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/BillingPortalSessionRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/BillingPortalSessionRequestRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BillingPortalSessionResponse'
          description: ''
        '400':
          description: Validation error (e.g. malformed return_url).
        '404':
          description: Client not found or not accessible to the caller.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BillingPortalSessionError'
          description: No active Stripe customer for this client yet.
        '502':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BillingPortalSessionError'
          description: Stripe API error -- portal session could not be created.
        '503':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BillingPortalSessionError'
          description: >-
            Stripe is wired but credentials are not configured in this
            environment.
  /api/v1/client/{client_uid}/invite-codes/:
    get:
      operationId: client_invite_codes_list
      description: |-
        List or mint invite codes scoped to a single Client (GH-649).

        GET  /api/v1/client/<client_uid>/invite-codes/
            Returns the codes attributed to ``client_uid``.  Same shape as
            the legacy ``GET /api/v1/account/invite-codes/`` endpoint but
            always pre-filtered to the URL Client so the frontend doesn't
            have to know whether the caller is staff (and would otherwise
            see codes for every Client they can access).

            Visible to any user who can read the Client's data -- the
            existing :func:`_user_can_see_client_codes` rule applies (staff
            OR an active ``ClientMembership`` at this Client).  Cross-tenant
            UID guesses get a 404 indistinguishable from a non-existent
            Client.

        POST /api/v1/client/<client_uid>/invite-codes/
            Mints a fresh code attributed to ``client_uid`` with
            ``created_by`` set to the caller.  Gated by
            :func:`_user_can_mint_for_client` (staff / OWNER / admin / OM /
            dentist) and rate-limited per Client to
            :data:`CLIENT_INVITE_CODE_MONTHLY_LIMIT` codes per calendar
            month.  Hit the cap and the response is 429 with body
            ``{"detail": "...", "limit": <int>, "period_start": "<iso>"}``.
      parameters:
        - in: path
          name: client_uid
          schema:
            type: string
            format: uuid
          required: true
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
      tags:
        - Account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedInviteCodeList'
          description: ''
    post:
      operationId: client_invite_codes_create
      description: |-
        List or mint invite codes scoped to a single Client (GH-649).

        GET  /api/v1/client/<client_uid>/invite-codes/
            Returns the codes attributed to ``client_uid``.  Same shape as
            the legacy ``GET /api/v1/account/invite-codes/`` endpoint but
            always pre-filtered to the URL Client so the frontend doesn't
            have to know whether the caller is staff (and would otherwise
            see codes for every Client they can access).

            Visible to any user who can read the Client's data -- the
            existing :func:`_user_can_see_client_codes` rule applies (staff
            OR an active ``ClientMembership`` at this Client).  Cross-tenant
            UID guesses get a 404 indistinguishable from a non-existent
            Client.

        POST /api/v1/client/<client_uid>/invite-codes/
            Mints a fresh code attributed to ``client_uid`` with
            ``created_by`` set to the caller.  Gated by
            :func:`_user_can_mint_for_client` (staff / OWNER / admin / OM /
            dentist) and rate-limited per Client to
            :data:`CLIENT_INVITE_CODE_MONTHLY_LIMIT` codes per calendar
            month.  Hit the cap and the response is 429 with body
            ``{"detail": "...", "limit": <int>, "period_start": "<iso>"}``.
      parameters:
        - in: path
          name: client_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Account
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InviteCodeMintResponse'
          description: ''
        '403':
          description: Caller is not authorised to mint codes for this Client.
        '404':
          description: Client not found or inactive.
        '429':
          description: Per-Client monthly mint quota exceeded.
  /api/v1/clients/{client_uid}/reports/adjustments/:
    get:
      operationId: clients_reports_adjustments_retrieve
      description: Per-practice adjustments breakdown by ``adj_type``.
      parameters:
        - in: path
          name: client_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: end_date
          schema:
            type: string
            format: date
          description: Inclusive end date (YYYY-MM-DD). Defaults to today.
        - in: query
          name: start_date
          schema:
            type: string
            format: date
          description: >-
            Inclusive start date (YYYY-MM-DD). Defaults to 30 days before
            end_date.
      tags:
        - DSO Reports
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdjustmentsResponse'
          description: ''
  /api/v1/clients/{client_uid}/reports/ar-aging/:
    get:
      operationId: clients_reports_ar_aging_retrieve
      description: Four-bucket AR aging totals + per-practice breakdown.
      parameters:
        - in: query
          name: as_of
          schema:
            type: string
            format: date
          description: >-
            Reference date for point-in-time aggregates (YYYY-MM-DD). Defaults
            to today.
        - in: path
          name: client_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - DSO Reports
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ARAgingResponse'
          description: ''
  /api/v1/clients/{client_uid}/reports/case-acceptance/:
    get:
      operationId: clients_reports_case_acceptance_retrieve
      description: |-
        Per-practice case-acceptance rate with cross-practice rollup.

        api#506 adds two pieces on top of the original contract:

        * Per-practice ``same_visit_accepted_pct`` and
          ``delayed_accepted_pct`` -- both expressed as a percentage of the
          *presented* amount so a stacked bar can read them directly.
        * ``?procedure_category=`` -- comma-separated set of
          :class:`apps.clinical.models.ServiceType` codes (e.g.
          ``major,endodontics``) that narrows the queryset to plan items
          whose linked :class:`ProcedureCode.service_type` is in the set.
          Items with no linked code are excluded once a filter is supplied.
      parameters:
        - in: path
          name: client_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: end_date
          schema:
            type: string
            format: date
          description: Inclusive end date (YYYY-MM-DD). Defaults to today.
        - in: query
          name: procedure_category
          schema:
            type: string
          description: >-
            Comma-separated list of ServiceType codes (diagnostic, preventive,
            basic, major, endodontics, periodontics, oral_surgery, orthodontics,
            prosthodontics, adjunctive, other).  Narrows the queryset to plan
            items whose linked ProcedureCode.service_type falls in the supplied
            set.
        - in: query
          name: start_date
          schema:
            type: string
            format: date
          description: >-
            Inclusive start date (YYYY-MM-DD). Defaults to 30 days before
            end_date.
      tags:
        - DSO Reports
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CaseAcceptanceResponse'
          description: ''
  /api/v1/clients/{client_uid}/reports/hygiene-recare/:
    get:
      operationId: clients_reports_hygiene_recare_retrieve
      description: |-
        Per-practice hygiene-recare compliance counts and rate.

        api#507 adds two pieces:

        * ``avg_days_late_among_non_compliant`` per practice -- the mean
          number of days past ``date_due`` across the practice's overdue
          patients.  Surfaces "narrowly missed" vs "long lost".
        * ``?include_without_active_recare=true`` -- widens the
          denominator to include active patients with no recall row at
          all.  Returned as a separate ``without_active_recare_count``
          bucket so consumers can show "of every active patient, what
          fraction are actually on a recare track".
      parameters:
        - in: query
          name: as_of
          schema:
            type: string
            format: date
          description: >-
            Reference date for point-in-time aggregates (YYYY-MM-DD). Defaults
            to today.
        - in: path
          name: client_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: include_without_active_recare
          schema:
            type: boolean
          description: >-
            When ``true``, widens the denominator to include active patients
            with no active recall row.  These patients show up in a separate
            ``without_active_recare_count`` bucket and depress the compliance
            rate accordingly.  Default ``false`` preserves the legacy
            denominator (active recall rows only).
      tags:
        - DSO Reports
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HygieneRecareResponse'
          description: ''
  /api/v1/clients/{client_uid}/reports/overview/:
    get:
      operationId: clients_reports_overview_retrieve
      description: |-
        KPI tiles for the DSO landing page.

        Returns four numbers per practice -- today's production, the
        trailing-week production, current AR total, and the 30-day
        no-show rate -- plus an org-wide totals block that sums the
        dollar values and computes a weighted no-show rate so the
        rollup tile isn't dominated by an outlier practice.
      parameters:
        - in: query
          name: as_of
          schema:
            type: string
            format: date
          description: >-
            Reference date for point-in-time aggregates (YYYY-MM-DD). Defaults
            to today.
        - in: path
          name: client_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - DSO Reports
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OverviewResponse'
          description: ''
  /api/v1/clients/{client_uid}/reports/payer-mix/:
    get:
      operationId: clients_reports_payer_mix_retrieve
      description: Per-practice payer mix derived from each patient's primary insurance.
      parameters:
        - in: path
          name: client_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - DSO Reports
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayerMixResponse'
          description: ''
  /api/v1/clients/{client_uid}/reports/production-by-provider/:
    get:
      operationId: clients_reports_production_by_provider_retrieve
      description: Per-practice / per-provider production with production-per-hour.
      parameters:
        - in: path
          name: client_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: end_date
          schema:
            type: string
            format: date
          description: Inclusive end date (YYYY-MM-DD). Defaults to today.
        - in: query
          name: start_date
          schema:
            type: string
            format: date
          description: >-
            Inclusive start date (YYYY-MM-DD). Defaults to 30 days before
            end_date.
      tags:
        - DSO Reports
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductionByProviderResponse'
          description: ''
  /api/v1/clients/{client_uid}/reports/production-vs-collections/:
    get:
      operationId: clients_reports_production_vs_collections_retrieve
      description: Per-practice production / collections / adjustments table.
      parameters:
        - in: path
          name: client_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: end_date
          schema:
            type: string
            format: date
          description: Inclusive end date (YYYY-MM-DD). Defaults to today.
        - in: query
          name: start_date
          schema:
            type: string
            format: date
          description: >-
            Inclusive start date (YYYY-MM-DD). Defaults to 30 days before
            end_date.
      tags:
        - DSO Reports
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductionVsCollectionsResponse'
          description: ''
  /api/v1/desktop/documents/sessions/{session_uid}/:
    get:
      operationId: desktop_documents_sessions_retrieve
      description: Get capture session status for crash recovery.
      parameters:
        - in: path
          name: session_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - desktop
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentUploadSessionStatus'
          description: ''
  /api/v1/desktop/documents/sessions/{session_uid}/complete/:
    post:
      operationId: desktop_documents_sessions_complete_create
      description: |-
        Mark a desktop capture session as complete.

        Called by the desktop app when the user clicks "Done".
      parameters:
        - in: path
          name: session_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/desktop/documents/sessions/{session_uid}/upload/:
    post:
      operationId: desktop_documents_sessions_upload_create
      description: |-
        Upload a document to a desktop capture session.

        Called by the desktop app for each scanned/selected file. Files are
        stored as ``InboxItem`` rows on the session's practice with
        ``source=API`` and ``source_application`` pointing at the OAuth2 app
        that minted the upload token.
      parameters:
        - in: path
          name: session_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/desktop/documents/token-exchange/:
    post:
      operationId: desktop_documents_token_exchange_create
      description: >-
        Exchange a one-time capture token for an OAuth2 access token.


        Called by the desktop app after receiving the ``escape-documents://``
        URL.

        No prior authentication is required — the token IS the auth.


        A token may only be exchanged once. Replay or expired tokens return

        410 Gone and are audited via ``doccenter.upload_session_rejected``.
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
        - {}
      responses:
        '200':
          description: No response body
  /api/v1/feedback/:
    get:
      operationId: feedback_list
      description: >-
        Filters: ``?status=`` (new / triaged / resolved), ``?user=`` (submitter
        UUID), ``?practice=`` (practice UUID), ``?client=`` (client UUID),
        ``?occurred_from=`` / ``?occurred_to=`` (calendar-day range on
        ``created``, UTC, inclusive of both endpoints, ``YYYY-MM-DD``).  Ordered
        by ``-created`` (newest first).  Returns the compact list shape; open
        the detail view to read the full submission including the signed
        screenshot URL.
      summary: List feedback submissions (internal staff only).
      parameters:
        - in: query
          name: client
          schema:
            type: string
            format: uuid
          description: Filter by client UUID.
        - in: query
          name: occurred_from
          schema:
            type: string
            format: date
          description: >-
            Filter rows whose ``created`` timestamp (UTC) falls on or after this
            calendar date (``YYYY-MM-DD``).  Invalid date format returns
            ``400``.
        - in: query
          name: occurred_to
          schema:
            type: string
            format: date
          description: >-
            Filter rows whose ``created`` timestamp (UTC) falls on or before
            this calendar date (``YYYY-MM-DD``); the endpoint is inclusive of
            the entire day.  Invalid date format returns ``400``.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: query
          name: practice
          schema:
            type: string
            format: uuid
          description: Filter by practice UUID.
        - in: query
          name: status
          schema:
            type: string
          description: 'Filter by triage status: ``new`` / ``triaged`` / ``resolved``.'
        - in: query
          name: user
          schema:
            type: string
            format: uuid
          description: Filter by submitter user UUID.
      tags:
        - feedback
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedFeedbackListList'
          description: ''
    post:
      operationId: feedback_create
      description: >-
        Accepts a feedback submission from the staff feedback widget.  Body may
        be either ``application/json`` (no screenshot) or
        ``multipart/form-data`` (screenshot attached as the ``screenshot``
        field).  The submitting user is set server-side from ``request.user`` --
        a client-supplied ``user`` value is ignored.  The row is treated as PHI
        on the read side.
      summary: Submit in-app feedback (any authenticated staff user).
      tags:
        - feedback
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FeedbackCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/FeedbackCreateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeedbackDetail'
          description: ''
  /api/v1/feedback/{feedback_uid}/:
    get:
      operationId: feedback_retrieve
      description: >-
        Returns the full row, including a short-lived signed URL for the
        screenshot when one was attached.  Every read fires a
        ``feedback.viewed`` audit event because the row may carry incidentally
        captured PHI.
      summary: Read a single feedback submission (internal staff only).
      parameters:
        - in: path
          name: feedback_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - feedback
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeedbackDetail'
          description: ''
  /api/v1/feedback/{feedback_uid}/transition/:
    post:
      operationId: feedback_transition_create
      description: >-
        Valid target statuses: ``new`` / ``triaged`` / ``resolved``.  Side
        effects: moving out of ``new`` for the first time stamps ``triaged_by``
        / ``triaged_at``; moving to ``resolved`` stamps ``resolved_by`` /
        ``resolved_at``; bouncing back to ``new`` or ``triaged`` clears the
        resolved-stamp pair so the field always reflects the current state.
      summary: Transition a feedback row's status (internal staff only).
      parameters:
        - in: path
          name: feedback_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - feedback
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FeedbackTransitionRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/FeedbackTransitionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/FeedbackTransitionRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeedbackDetail'
          description: ''
        '400':
          description: Invalid target status.
  /api/v1/fhir/export:
    get:
      operationId: fhir_bulk_export_kickoff
      description: >-
        System-level FHIR Bulk Data Access $export.  Returns 202 with a
        Content-Location header pointing at the poll URL.  The job runs
        asynchronously and writes one NDJSON file per requested resource type to
        private object storage.
      summary: Kick off a FHIR Bulk Data ($export) job.
      parameters:
        - in: header
          name: X-Practice-UID
          schema:
            type: string
            format: uuid
          description: >-
            Practice UUID; required when the token grants access to multiple
            practices.
        - in: query
          name: _since
          schema:
            type: string
            format: date-time
          description: >-
            Only emit resources modified at or after this ISO-8601 datetime
            (must be tz-aware).
        - in: query
          name: _type
          schema:
            type: string
          description: >-
            Comma-separated FHIR resource types to export. Defaults to all
            supported types.
      tags:
        - FHIR Bulk Data
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '202':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
  /api/v1/fhir/export-status/{job_uid}:
    get:
      operationId: fhir_bulk_export_status
      description: >-
        Returns 202 + X-Progress header while the job is running; 200 with the
        IG-defined manifest when complete; 200 with an empty output when the job
        was cancelled or failed.
      summary: Poll a FHIR Bulk Data ($export) job.
      parameters:
        - in: header
          name: X-Practice-UID
          schema:
            type: string
            format: uuid
          description: >-
            Practice UUID; required when the token grants access to multiple
            practices.
        - in: path
          name: job_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - FHIR Bulk Data
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
        '202':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
    delete:
      operationId: fhir_bulk_export_cancel
      description: >-
        Marks the job as cancelled.  The Celery worker checks the row's status
        between resource types and exits cleanly.
      summary: Cancel a FHIR Bulk Data ($export) job.
      parameters:
        - in: header
          name: X-Practice-UID
          schema:
            type: string
            format: uuid
          description: >-
            Practice UUID; required when the token grants access to multiple
            practices.
        - in: path
          name: job_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - FHIR Bulk Data
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '202':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
        '404':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
  /api/v1/forms/kiosk/{token}/:
    get:
      operationId: forms_kiosk_retrieve
      description: |-
        ``GET /api/v1/forms/kiosk/<token>/``

        Token-scoped read.  Returns the kiosk session state, the currently-
        loaded sendout (if any), and the templates the device should render.
        A request against an ended / idled-out session returns 410 Gone.

        The state read DOES bump ``last_activity_at`` — a tablet that polls
        every few seconds keeps the session alive, which is the intended
        UX: idle timeout only fires when the patient walks away.
      parameters:
        - in: path
          name: token
          schema:
            type: string
          required: true
      tags:
        - Forms
      security:
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/forms/kiosk/{token}/submit/:
    post:
      operationId: forms_kiosk_submit_create
      description: >-
        ``POST /api/v1/forms/kiosk/<token>/submit/``


        Body shape mirrors :class:`PublicSubmitView` — ``{ "submissions": [...]
        }``.

        Differences from the patient-portal public submit:


        * The session must be active; an expired session returns 410 Gone.

        * A loaded :class:`FormSendOut` is required; submitting against a
          session with ``current_sendout=None`` is a 400 (the kiosk shouldn't
          be rendering a form yet anyway).
        * Submissions are tagged ``submitted_via='kiosk'``; the
        ``submitted_via``
          field in the payload is ignored if present.
        * The sendout's ``status`` is NOT flipped to ``submitted``: staff may
          want to load another form on the same kiosk for the same patient,
          so closing the sendout here would force the staff client to mint a
          fresh sendout for every form.  Staff close out the sendout from
          their UI when the visit ends.
        * Every accepted submission stamps ``last_activity_at`` on the kiosk
          session so a fast-filling patient keeps the deadline pushed out.

        Note: the chart-attach + mapping-proposal signals fire on every

        :class:`FormSubmission` regardless of channel, so kiosk submissions

        get the same downstream pipeline (PDF in the chart, staged patient-

        record writes) as portal / signed-link submissions.
      parameters:
        - in: path
          name: token
          schema:
            type: string
          required: true
      tags:
        - Forms
      security:
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/forms/public/{token}/:
    get:
      operationId: forms_public_retrieve
      description: |-
        GET /api/v1/forms/public/<token>/ — render template + in-progress state.

        No auth.  Open access is intentional: the token IS the credential.  The
        token is single-use only in the sense that ``status=submitted`` rejects
        further submissions; reads (this endpoint) stay available so the patient
        can review what they sent.
      parameters:
        - in: path
          name: token
          schema:
            type: string
          required: true
      tags:
        - Forms
      security:
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/forms/public/{token}/submit/:
    post:
      operationId: forms_public_submit_create
      description: |-
        POST /api/v1/forms/public/<token>/submit/

        Body shape:
            ``{ "submissions": [
                { "template_id": "<uuid>",
                  "submitted_via": "link|portal|kiosk",
                  "responses": [ { "field_id": "<uuid>", "value": "..." } ] }
            ] }``

        The endpoint pins each submission to the exact :class:`FormTemplate` row
        served by the public GET endpoint, so a staff edit landing mid-fill does
        not retroactively rewrite the patient's responses.
      parameters:
        - in: path
          name: token
          schema:
            type: string
          required: true
      tags:
        - Forms
      security:
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/oauth/app-launch-token/:
    post:
      operationId: oauth_app_launch_token_create
      description: |-
        Mint a short-lived launch JWT bound to (user, application, practice).

        The returned JWT carries ``aud=<client_id>``, ``sub=user:<uid>``,
        ``practice=<uid>``, ``exp`` (now + 60 s), and a fresh ``jti``.  The
        ``jti`` is recorded in :class:`LaunchTokenJTI` *before* the response
        is returned so the exchange endpoint can detect replays.

        Permission stack:

        1. ``IsAuthenticated`` -- session or bearer; practically staff
           sessions from the web nine-dots.
        2. ``IsStaffUser`` -- patient users cannot launch 3rd-party apps.
        3. Explicit check that the authenticated user has practice access.
        4. Explicit check that the user's active
           :class:`OAuthPracticeGrant` authorizes this app for this
           practice.
        5. Rate limit (see :class:`LaunchTokenCreateThrottle`).
      summary: Mint a single-use launch JWT for a 3rd-party app
      tags:
        - OAuth
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppLaunchTokenRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AppLaunchTokenRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/AppLaunchTokenRequestRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppLaunchTokenResponse'
          description: ''
        '403':
          description: No response body
        '404':
          description: No response body
        '429':
          description: No response body
  /api/v1/oauth/me/delegated-apps/:
    get:
      operationId: oauth_me_delegated_apps_retrieve
      description: >-
        Returns every OAuth application for which the caller holds at least one
        active ``act_on_behalf``-scoped access token, enriched with
        ``first_granted_at`` from the backing grant and ``last_used_at`` from
        the usage-metric rollup.  Powers the Connected Apps settings page.  No
        practice filter -- delegated apps are scoped to the user, not to any
        single practice membership.
      summary: List the 3rd-party apps with delegated access to your account
      tags:
        - OAuth
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DelegatedAppListResponse'
          description: ''
  /api/v1/oauth/me/delegated-apps/{app_id}/revoke/:
    post:
      operationId: oauth_me_delegated_apps_revoke_create
      description: >-
        Invalidates every ``act_on_behalf``-scoped grant and token held by the
        signing-in user for the target app.  The app's practice-level
        authorization is **not** affected -- that is governed separately by the
        practice owner via the Security Nexus.
      summary: Revoke a 3rd-party app's delegated access to your account
      parameters:
        - in: path
          name: app_id
          schema:
            type: string
          required: true
      tags:
        - OAuth
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DelegatedAppRevokeResponse'
          description: ''
        '404':
          description: No response body
  /api/v1/oauth/tablet-pair/:
    post:
      operationId: oauth_tablet_pair_create
      description: |-
        Public pair-completion endpoint.

        No authentication is required -- the unguessable pair_token IS the
        auth.  The endpoint:

        1. Verifies the JWT signature + expiry.
        2. Confirms the named practice exists (404 cross-tenant guess
           protection -- a JWT for a since-deleted practice resolves to
           a clean 404 instead of a 500).
        3. Atomically:
           a. Inserts :class:`PairTokenRedemption` (Postgres unique
              constraint races a second redemption into 410).
           b. Creates the :class:`Tablet` row from the JWT claims +
              posted public key.
           c. Mints a DOT AccessToken + RefreshToken bound to the
              ``escape-kiosk-tablet`` Application and the new Tablet.
        4. Returns the access + refresh token values, the tablet id, the
           tablet name, and the practice id so the device can begin
           making authenticated calls immediately.
      tags:
        - Tablets
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PairCompletionRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PairCompletionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PairCompletionRequest'
        required: true
      security:
        - bearerAuth: []
        - oauth2: []
        - {}
      responses:
        '201':
          description: Pair completed; tokens returned for storage on the device.
        '401':
          description: Pair token is missing, malformed, expired, or invalid.
        '410':
          description: Pair token was already redeemed (single-use).
  /api/v1/patient-feedback/:
    get:
      operationId: patient_feedback_list
      description: >-
        Filters: ``?status=`` (new / triaged / resolved), ``?user=`` (submitter
        UUID), ``?patient=`` (patient UUID), ``?practice=`` (practice UUID),
        ``?client=`` (client UUID).  Ordered by ``-created`` (newest first). 
        Returns the compact list shape; open the detail view for the full row.
      summary: List patient-feedback submissions (internal staff only).
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
      tags:
        - patient-feedback
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPatientFeedbackListList'
          description: ''
    post:
      operationId: patient_feedback_create
      description: >-
        Accepts a feedback submission from the patient portal.  Body is
        ``application/json`` with ``{title, details, page_url}``; ``viewport``
        and ``user_agent`` are optional.  The submitting user is set server-side
        from ``request.user`` -- a client-supplied ``user`` value is ignored. 
        The matched per-practice ``Patient`` record is resolved automatically by
        case-insensitive email match.  The row is treated as PHI on the read
        side.  Throttled to 5 submissions per hour per portal user.
      summary: Submit patient-portal feedback (patient users only).
      tags:
        - patient-feedback
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientFeedbackCreateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientFeedbackCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientFeedbackCreateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientFeedbackDetail'
          description: ''
        '429':
          description: Rate limit exceeded (5/hour per patient).
  /api/v1/patient-feedback/{patient_feedback_uid}/:
    get:
      operationId: patient_feedback_retrieve
      description: >-
        Returns the full row.  Every read fires a ``patient_feedback.viewed``
        audit event because the row may carry incidentally captured PHI.
      summary: Read a single patient-feedback submission (internal staff only).
      parameters:
        - in: path
          name: patient_feedback_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - patient-feedback
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientFeedbackDetail'
          description: ''
  /api/v1/patient-feedback/{patient_feedback_uid}/transition/:
    post:
      operationId: patient_feedback_transition_create
      description: >-
        Valid target statuses: ``new`` / ``triaged`` / ``resolved``.  Side
        effects: moving out of ``new`` for the first time stamps ``triaged_by``
        / ``triaged_at``; moving to ``resolved`` stamps ``resolved_by`` /
        ``resolved_at``; bouncing back to ``new`` or ``triaged`` clears the
        resolved-stamp pair so the field always reflects the current state.
      summary: Transition a patient-feedback row's status (internal staff only).
      parameters:
        - in: path
          name: patient_feedback_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - patient-feedback
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientFeedbackTransitionRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientFeedbackTransitionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientFeedbackTransitionRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientFeedbackDetail'
          description: ''
        '400':
          description: Invalid target status.
  /api/v1/payment/:
    get:
      operationId: payment_list
      description: List payment history for the authenticated patient.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
      tags:
        - payment
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPaymentIntentList'
          description: ''
  /api/v1/payment/create/:
    post:
      operationId: payment_create_create
      description: |-
        Create a Stripe PaymentIntent and return the client secret.

        POST /api/v1/payment/create/

        The frontend takes ``client_secret`` and confirms the card via
        Stripe.js so raw PAN never touches our servers (SAQ-A scope).  The
        matching ``PaymentIntent`` row is created in PENDING state and
        transitioned to SUCCEEDED / FAILED by the webhook handler when the
        confirmation lifecycle completes.
      tags:
        - Payments
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePaymentIntentRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/CreatePaymentIntentRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/CreatePaymentIntentRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatePaymentIntentResponse'
          description: ''
        '400':
          description: Validation error (e.g. amount below $0.50).
        '502':
          description: Stripe API error — charge could not be created.
        '503':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeNotConfigured'
          description: ''
  /api/v1/payment/webhook/stripe/:
    post:
      operationId: payment_webhook_stripe_create
      description: |-
        Handle Stripe webhook events.

        POST /api/v1/payment/webhook/stripe/

        The Stripe SDK's ``Webhook.construct_event`` does signature +
        timestamp verification, so we delegate to it and treat any
        ``ValueError`` as a 400.  Idempotency is enforced via the
        :class:`WebhookDelivery` table: the first time we see an event id
        we record it and run the handler, every subsequent delivery
        short-circuits to a 200 OK without side effects.
      tags:
        - Payments
      security:
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
          description: ''
        '400':
          description: Invalid signature or malformed payload.
        '503':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeNotConfigured'
          description: ''
  /api/v1/portal/appointments/:
    get:
      operationId: portal_appointments_list
      description: >-
        Returns appointments scoped to the authenticated patient -- resolved
        server-side from the access token's user.  No ``patient_id`` query
        parameter is accepted (cross-patient access vector).


        Response items carry the patient-portal-safe shape: ``uid``,
        ``start_time`` (ISO 8601 UTC), ``duration_minutes``, ``provider_name``,
        ``procedure_description``, and ``status``.  Clinical notes, treatment
        plan IDs, dollars_scheduled, and the long-tail status taxonomy are
        deliberately omitted.


        Filters: ``?from=YYYY-MM-DD`` / ``?to=YYYY-MM-DD`` (inclusive both ends,
        **practice timezone**), ``?status=scheduled,confirmed`` (comma-separated
        whitelist).  Page size capped at 100 via
        ``StandardPageNumberPagination``.
      summary: List the authenticated patient's appointments.
      parameters:
        - in: query
          name: from
          schema:
            type: string
            format: date
          description: >-
            Earliest appointment date to include (YYYY-MM-DD, practice
            timezone).
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: query
          name: status
          schema:
            type: string
          description: >-
            Comma-separated whitelist of patient-facing statuses: ``scheduled``,
            ``confirmed``, ``arrived``, ``completed``, ``cancelled``,
            ``no_show``.
        - in: query
          name: to
          schema:
            type: string
            format: date
          description: >-
            Latest appointment date to include (YYYY-MM-DD, practice timezone,
            inclusive).
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPortalAppointmentList'
          description: ''
        '400':
          description: Malformed query parameter.
        '401':
          description: Authentication required.
        '403':
          description: Not a portal (patient) user.
        '404':
          description: No active patient record found for this portal user.
  /api/v1/portal/dashboard-counts/:
    get:
      operationId: portal_dashboard_counts_retrieve
      description: >-
        Returns a single envelope of integer counts the patient-portal home
        screen needs to render its card badges, replacing N full-list ``GET``s
        the dashboard previously performed just to derive ``.length``.


        Counts are scoped to the authenticated patient -- resolved server-side
        from the access token's user via case-insensitive email match against
        the active Patient rows.  No ``patient_id`` query parameter is honoured.


        The response carries **no PHI** -- only integers -- so the endpoint is
        safe to cache for short windows in upstream caches.  We set
        ``Cache-Control: private, max-age=15`` to blunt thundering-herd from a
        tab the patient has left open; the SPA pairs this with TanStack Query's
        ``staleTime: 30_000`` for defence-in-depth.


        Future counts (unread messages, outstanding forms, uploaded-document
        approvals) will land here as new integer fields once the matching
        patient-facing surfaces exist server-side.  Adding a field is
        backwards-compatible for the SPA.
      summary: Batched badge counts for the patient-portal dashboard.
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortalDashboardCounts'
              examples:
                TypicalDashboardCounts:
                  value:
                    upcoming_appointments: 2
                  summary: One cleaning + one new-patient exam on the calendar
                  description: >-
                    Single-shot reply used to populate every count badge on the
                    patient-portal home screen.  The integer is a true
                    ``COUNT(*)`` over the same filter the dashboard previously
                    applied client-side after fetching the full list -- so the
                    badge value is identical across the swap, just one network
                    round-trip cheaper.
                NoAppointmentsYet:
                  value:
                    upcoming_appointments: 0
                  summary: Zero-state envelope
                  description: >-
                    The envelope keeps every key even when every count is zero
                    -- the SPA reads ``upcoming_appointments`` unconditionally,
                    so a missing key would be a harder error than a present
                    zero.
          description: ''
        '401':
          description: Authentication required.
        '403':
          description: Not a portal (patient) user.
        '404':
          description: No active patient record found for this portal user.
  /api/v1/portal/installed-apps/:
    get:
      operationId: portal_installed_apps_list
      description: >-
        Returns OAuth applications the current user has consented to that have
        declared the requested slot.  The slot query parameter is required and
        must come from the canonical allowlist; unknown slots return 400.
      summary: List installed OAuth apps for the current user
      parameters:
        - in: query
          name: slot
          schema:
            type: string
          description: >-
            Slot name to filter on. Allowed values: patient.detail.actions,
            patient.detail.tab, practice.bar.nine-dots,
            schedule.appointment.actions.
          required: true
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/InstalledApp'
          description: ''
        '400':
          description: Missing or unknown slot.
  /api/v1/portal/messaging/conversation/:
    get:
      operationId: portal_messaging_conversation_list
      description: >-
        Returns the conversations the authenticated patient is an explicit
        participant of, newest activity first.  Each row carries
        ``unread_count`` -- the count of messages created after the patient's
        ``last_read_at`` bookmark -- so the SPA dashboard's
        ``usePatientUnreadMessageCount`` can sum across rows for the badge
        without a second round-trip.


        Cross-patient isolation: the queryset is filtered by
        ``ConversationParticipant.user = request.user``.  A patient with no
        participant rows sees an empty page; nobody can see another patient's
        threads.  Same 401 / 403 / 404 contract as the sibling
        ``/portal/dashboard-counts/`` and ``/portal/appointments/`` endpoints.
      summary: List the calling patient's conversations.
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ConversationList'
          description: ''
        '401':
          description: Authentication required.
        '403':
          description: Not a portal (patient) user.
    post:
      operationId: portal_messaging_conversation_create
      description: >-
        Creates a new ``direct`` ``Conversation`` rooted at the patient's
        resolved Practice, attaches the calling patient as a
        ``ConversationParticipant``, attaches the practice's staff messaging
        recipients (active ``ADMIN`` / ``OFFICE_MANAGER`` memberships) as
        additional participants, and persists the first message in a single
        transaction.  Returns 201 with the conversation envelope (same shape as
        the detail GET).


        Staff routing: if the practice has named ``messaging_inbox_owners``
        configured (GH-695), only those active users are CC'd.  Otherwise the
        fan-out is every active admin / office-manager membership on the
        practice -- the simplest correct default for practices that haven't set
        an owner yet.  A small admin pool can always triage and reassign; a
        single named owner that happens to be on vacation cannot, so the
        fallback also kicks in when every named owner has been deactivated.


        Rate limited per portal user at ``portal_messaging_start_conversation``
        (default 1 / 5 minutes) -- see ``apps.portal.throttles``.


        Audit: ``messaging.conversation_started_by_patient`` and
        ``messaging.message_sent_by_patient`` are both emitted with
        ``subject_user=request.user``.  Body and subject text are NEVER stored
        on the audit document -- only their lengths.
      summary: Start a new conversation with the calling patient's practice.
      tags:
        - Portal
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/_PatientStartConversationRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/_PatientStartConversationRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/_PatientStartConversationRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationDetail'
          description: ''
        '400':
          description: Validation error (blank body, body too long).
        '401':
          description: Authentication required.
        '403':
          description: Not a portal (patient) user.
        '404':
          description: No active patient record found for this account.
        '429':
          description: Start-conversation rate exceeded.
  /api/v1/portal/messaging/conversation/{conversation_id}/:
    get:
      operationId: portal_messaging_conversation_retrieve
      description: >-
        Returns the conversation with the given id **only if** the authenticated
        patient is a participant.  Returns 404 otherwise -- existence-leak
        ("this conversation exists, you just can't see it") is itself a PHI
        signal so we never distinguish 403 from 404 here.
      summary: Retrieve one of the calling patient's conversations.
      parameters:
        - in: path
          name: conversation_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationDetail'
          description: ''
        '401':
          description: Authentication required.
        '403':
          description: Not a portal (patient) user.
        '404':
          description: No matching conversation for this patient.
  /api/v1/portal/messaging/conversation/{conversation_id}/messages/:
    get:
      operationId: portal_messaging_message_list
      description: >-
        Returns messages in the named conversation, chronological order (oldest
        first), paginated.  Cross-patient isolation: the conversation must have
        the calling patient as an explicit participant or the endpoint returns
        404 before issuing the message query.


        Emits a single ``messaging.viewed_by_patient`` audit event per request
        (subject_user=request.user, resource_id=conversation_id).  Message
        bodies are NOT included in audit metadata -- only the conversation id
        and the count of messages returned -- because the bodies are PHI and the
        audit document index isn't the right place for them.
      summary: List messages in one of the calling patient's conversations.
      parameters:
        - in: path
          name: conversation_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Message'
          description: ''
        '401':
          description: Authentication required.
        '403':
          description: Not a portal (patient) user.
        '404':
          description: No matching conversation for this patient.
    post:
      operationId: portal_messaging_message_send
      description: >-
        Persists a new ``Message`` in the named conversation with
        ``sender=request.user``.  Returns 201 with the created message envelope
        (same shape the message-list endpoint returns) on success.


        Cross-patient isolation: the conversation must have the calling patient
        as an explicit participant or the endpoint returns 404 (not 403).  Same
        existence-leak rationale as the rest of the patient-portal messaging
        surface.


        WebSocket broadcast: persistence triggers the existing ``message.new``
        post-save signal in ``apps.messaging.signals`` so any staff window
        watching the conversation sees the new message in real time.  No
        additional broadcast is needed in the view path.


        Rate limited per portal user at ``portal_messaging_send`` (default
        30/hour) -- see ``apps.portal.throttles``.


        Audit: ``messaging.message_sent_by_patient`` with
        ``subject_user=request.user``.  Body length is reported in metadata but
        the body itself is NEVER stored on the audit document -- the body is
        PHI.
      summary: Send a message in one of the calling patient's conversations.
      parameters:
        - in: path
          name: conversation_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/_PatientSendMessageRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/_PatientSendMessageRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/_PatientSendMessageRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
          description: ''
        '400':
          description: Validation error (blank body, body too long).
        '401':
          description: Authentication required.
        '403':
          description: Not a portal (patient) user.
        '404':
          description: No matching conversation for this patient.
        '429':
          description: Send rate exceeded.
  /api/v1/portal/messaging/conversation/{conversation_id}/read/:
    patch:
      operationId: portal_messaging_conversation_mark_read
      description: >-
        Bumps ``ConversationParticipant.last_read_at`` for the calling patient
        on the named conversation to ``now()``.  Subsequent calls to the list
        endpoint will report ``unread_count: 0`` for this row until a new
        message arrives.


        Returns 404 if the calling patient is not an explicit participant -- the
        staff side's ``MarkConversationReadView`` auto-creates a participant row
        for operatory conversations, but the patient side never does: operatory
        threads are not patient-visible, so a missing participant row on the
        patient side always means the patient has no business in this thread.


        Emits ``messaging.conversation_read_by_patient`` with
        ``subject_user=request.user``.
      summary: Mark a conversation read for the calling patient.
      parameters:
        - in: path
          name: conversation_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: '`{"status": "ok"}`'
        '401':
          description: Authentication required.
        '403':
          description: Not a portal (patient) user.
        '404':
          description: No matching conversation for this patient.
  /api/v1/portal/messaging/unread-count/:
    get:
      operationId: portal_messaging_unread_count
      description: >-
        Returns ``{"unread": N}`` -- the sum across the patient's conversations
        of messages created after each thread's ``last_read_at`` bookmark. 
        Sibling helper to the ``/portal/dashboard-counts/`` envelope (api#682);
        the patients-side dashboard hook today derives the badge by summing
        ``unread_count`` across the conversation list, but this single-integer
        endpoint exists so future callers (e.g. a header badge that doesn't need
        the full list) can ask for just the number.


        The response carries no PHI -- a single integer -- so it is safe to
        cache briefly.  Returns ``Cache-Control: private, max-age=15`` matching
        ``/portal/dashboard-counts/``.
      summary: Total unread message count for the calling patient.
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: '`{"unread": 42}`'
        '401':
          description: Authentication required.
        '403':
          description: Not a portal (patient) user.
  /api/v1/portal/practices/:
    get:
      operationId: portal_practices_list
      description: >-
        Returns active practices the authenticated user has access to via direct
        or client-level membership.  Accepts first-party JWT/session auth, or
        OAuth2 bearer tokens carrying ``reports:read``.  Unpaginated; the
        per-user practice list is always small.
      summary: List practices the current user can access
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Practice'
          description: ''
  /api/v1/portal/submission/{token}/:
    post:
      operationId: portal_submission_create
      description: |-
        Submit patient intake data via a one-time invite token.

        POST /api/v1/portal/submission/<token>/

        No authentication required. The token must be valid, not expired,
        and not already used. Stores the submitted data and marks the
        submission as submitted.
      parameters:
        - in: path
          name: token
          schema:
            type: string
          required: true
      tags:
        - Patient Submissions (Public)
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientSubmissionSubmitRequest'
            examples:
              ProspectivePatientSubmitsIntakeData:
                value:
                  submitted_data:
                    first_name: John
                    last_name: Doe
                    email: john.doe@example.com
                    date_of_birth: '1990-04-12'
                    gender: M
                    phone_home: '+15555550100'
                    phone_mobile: '+15555550101'
                    phone_work: ''
                    address: 123 Example St
                    address2: ''
                    city: Springfield
                    state: IL
                    zip_code: '62701'
                    insurance:
                      carrier: Synthetic Dental PPO
                      subscriber_id: SYN-000-0001
                      group_number: GRP-9999
                    medical_history:
                      allergies: []
                      medications: []
                      conditions: []
                summary: POST /api/v1/portal/submission/<token>/
                description: >-
                  Public endpoint -- no authentication.  The invite token in the
                  URL authorises the write.  ``submitted_data`` is an open JSON
                  envelope; the practice's intake form decides its shape.
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientSubmissionSubmitRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientSubmissionSubmitRequest'
        required: true
      security:
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
              examples:
                SubmissionAccepted:
                  value:
                    detail: Submission received.
                  summary: 200 OK
                  description: The token is now marked used; replaying it returns 410 Gone.
          description: ''
        '404':
          description: Invalid invite token.
        '410':
          description: Invite has expired or already been used.
  /api/v1/practice/:
    get:
      operationId: practice_list
      description: List practices (admin-only).
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPracticeList'
          description: ''
  /api/v1/practice/{practice_uid}/:
    get:
      operationId: practice_retrieve
      description: >-
        Retrieve or update a specific practice by its UUID.


        Addressed by ``/api/v1/practice/<practice_uid>/`` (the non-scoped root).

        Access is restricted to authenticated staff users whose accessible

        practices include the target UID.  Cross-tenant UID guesses receive a

        404 so the endpoint does not leak the existence of other tenants'

        practices.


        PATCH-writable fields include the practice's name, address, phone,

        timezone, settings, and the three production-goal fields.  Every update

        emits a ``practice.updated`` audit event mirroring
        ``PracticeDetailView``.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Practices
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Practice'
          description: ''
    put:
      operationId: practice_update
      description: >-
        Retrieve or update a specific practice by its UUID.


        Addressed by ``/api/v1/practice/<practice_uid>/`` (the non-scoped root).

        Access is restricted to authenticated staff users whose accessible

        practices include the target UID.  Cross-tenant UID guesses receive a

        404 so the endpoint does not leak the existence of other tenants'

        practices.


        PATCH-writable fields include the practice's name, address, phone,

        timezone, settings, and the three production-goal fields.  Every update

        emits a ``practice.updated`` audit event mirroring
        ``PracticeDetailView``.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Practices
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PracticeRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PracticeRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PracticeRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Practice'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_partial_update
      description: >-
        Retrieve or update a specific practice by its UUID.


        Addressed by ``/api/v1/practice/<practice_uid>/`` (the non-scoped root).

        Access is restricted to authenticated staff users whose accessible

        practices include the target UID.  Cross-tenant UID guesses receive a

        404 so the endpoint does not leak the existence of other tenants'

        practices.


        PATCH-writable fields include the practice's name, address, phone,

        timezone, settings, and the three production-goal fields.  Every update

        emits a ``practice.updated`` audit event mirroring
        ``PracticeDetailView``.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Practices
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPracticeRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPracticeRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPracticeRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Practice'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/apps/:
    get:
      operationId: practice_apps_list
      description: >-
        Return the ordered list of app launchers for the practice's nine-dots
        menu.


        Filter chain (all must be true for an app to appear):


        1. The OAuth application has an ``OAuthAppProfile`` with
           ``approval_status == APPROVED`` and ``is_first_party == False``
           (escape-web itself must never show up as a launcher tile).
        2. The profile publishes a non-empty ``navbar_label`` — apps that
           haven't registered for the navbar stay invisible even if
           otherwise fully onboarded.
        3. There is at least one active ``OAuthPracticeGrant`` tying this
           application to this practice — only apps the practice has
           actually consented to appear in its launcher.
        4. No ``PracticeAppVisibility`` row hides the app for this practice.
           Absence of a row = default-visible.
      summary: List navbar-enabled third-party apps for a practice
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - OAuth Apps
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/NavbarApp'
          description: ''
  /api/v1/practice/{practice_uid}/apps/{client_id}/:
    patch:
      operationId: practice_apps_partial_update
      description: |-
        Practice-owner-only endpoint to hide/show a single app in the navbar.

        Upserts a ``PracticeAppVisibility`` row keyed on (practice, app).
        Only the parent client's OWNER role (or a superuser) may hit this.
        Other staff — including practice ADMINs — will receive a 403.
      summary: Toggle a third-party app's visibility in the practice's navbar
      parameters:
        - in: path
          name: client_id
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - OAuth Apps
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedAppVisibilityToggleRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedAppVisibilityToggleRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedAppVisibilityToggleRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppVisibilityToggle'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_apps_destroy
      description: |-
        Revoke all ``OAuthPracticeGrant`` rows for this app + practice.

        The access token cascade mirrors ``AppRevokeView`` but is scoped to
        a single practice rather than the entire org.  Requires
        ADMIN/OFFICE_MANAGER practice role or OWNER/TECHNICAL client role.
      summary: Disconnect (revoke) a third-party app from this practice
      parameters:
        - in: path
          name: client_id
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - OAuth Apps
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: No response body
  /api/v1/practice/{practice_uid}/apps/{client_id}/connect/:
    post:
      operationId: practice_apps_connect_create
      description: |-
        Self-registration endpoint for third-party OAuth apps.

        ``POST /api/v1/practice/<uid>/apps/<client_id>/connect/``

        A third-party SPA calls this endpoint **after** it has obtained an
        access token via the standard Authorization Code + PKCE flow, to
        create an ``OAuthPracticeGrant`` tying itself to this practice.
        Once the grant exists the app appears in the nine-dots launcher for
        the practice.

        Why not ``PracticeScopedMixin``?
        ``PracticeScopedMixin.get_practice()`` enforces that an active
        ``OAuthPracticeGrant`` already exists before the third-party caller
        can access any practice-scoped endpoint.  That is correct for
        *data* endpoints — but /connect/ is the endpoint that *creates*
        the grant.  Using the mixin here would be a chicken-and-egg
        deadlock: you can't connect without already being connected.

        Instead we:
        1. Verify the Bearer token is authenticated (``IsAuthenticated``).
        2. Directly look up the practice by URL UID and confirm the
           requesting user has a ``PracticeMembership`` in it.
        3. Create / reactivate the grant.

        Security
        --------
        - Requires a valid Bearer token (``IsAuthenticated``).
        - User must have an active ``PracticeMembership`` for the practice
          (``user.has_practice_access``).  This prevents a malicious token
          holder from manufacturing grants for practices they don't belong to.
        - The connecting ``client_id`` must be an **approved, non-first-party**
          application with a non-empty ``navbar_label``.
        - The grant is (user, application, practice) — multi-user practices
          each consent independently, which is the correct per-user model.

        Response
        --------
        * ``201 Created``  — grant created or reactivated.
        * ``200 OK``       — grant already active; idempotent re-connect.
        * ``403``          — user has no membership in this practice.
        * ``404``          — practice or app not found / not launcher-eligible.
      parameters:
        - in: path
          name: client_id
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - OAuth Apps
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/apps/{client_id}/launch/:
    post:
      operationId: practice_apps_launch_create
      description: |-
        Audited resolver for a single navbar launcher click.

        The web client calls this instead of rendering a raw ``<a href>`` so
        that every launch produces a server-side audit record — useful both
        for HIPAA (who opened which third-party tool) and for product
        analytics (which integrations are actually used).

        Success contract:

        * Returns ``200 {"target_url": "..."}`` — web opens it in a new tab
          with ``rel=noopener,noreferrer`` and window.open's 3rd-arg equivalent.
        * Emits an ``oauth_app.launch`` audit event *only* on the success
          path — failed or unauthorized launches are not logged as launches.

        Failure contract:

        * ``404`` for every navbar-ineligibility reason (unapproved,
          first-party, missing grant, hidden by visibility row, missing
          ``navbar_label`` or ``navbar_target_url``). We deliberately use
          the same status for every reason so the endpoint does not leak
          which apps the practice *has* granted — that information belongs
          only to the owner's visibility screen.
        * ``401`` for unauthenticated callers (DRF default).
        * ``403`` for users with no access to the scoped practice (raised by
          ``PracticeScopedMixin``).
      summary: Audited click-through for a third-party app launcher tile
      parameters:
        - in: path
          name: client_id
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - OAuth Apps
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppLaunchResponse'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: No response body
  /api/v1/practice/{practice_uid}/audit/reports/right-of-access/:
    post:
      operationId: audit_right_of_access_create
      description: |-
        Queue a new right-of-access report for a patient.

        Staff-only, practice-scoped.  The report generation runs asynchronously
        in a Celery task; callers poll the companion endpoint for completion.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Audit
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RightOfAccessRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/RightOfAccessRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/RightOfAccessRequestRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '202':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RightOfAccessAccepted'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/audit/reports/right-of-access/{report_uid}/:
    get:
      operationId: audit_right_of_access_detail
      description: |-
        Poll an existing right-of-access report.

        Returns status + (when READY) time-limited signed download URLs.
        404 on unknown report_uid; 403 if the report belongs to a different
        practice (enforced by ``PracticeScopedMixin`` on the URL practice_uid).
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: report_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Audit
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RightOfAccessStatus'
          description: ''
  /api/v1/practice/{practice_uid}/billing/adjustment/:
    get:
      operationId: practice_billing_adjustment_list
      description: |-
        Practice-wide adjustment list (api#185).

        Backs the Adjustments report (web#154).  Supports ``date_from`` /
        ``date_to`` (inclusive, practice TZ), repeated ``provider`` and
        ``adj_type`` params, and an optional single ``patient`` filter.
        POST creates an adjustment with ``patient`` provided in the request
        body (the URL is not patient-scoped).
      parameters:
        - in: query
          name: adj_type
          schema:
            type: array
            items:
              type: string
          description: >-
            Filter by the free-text ``adj_type`` string.  Repeat the parameter
            to include multiple types
            (``?adj_type=discount&adj_type=writeoff``).
        - in: query
          name: date_from
          schema:
            type: string
            format: date
          description: >-
            Inclusive lower bound on ``adj_date`` (YYYY-MM-DD, practice TZ). 
            Omit to return adjustments with any date.
        - in: query
          name: date_to
          schema:
            type: string
            format: date
          description: >-
            Inclusive upper bound on ``adj_date`` (YYYY-MM-DD, practice TZ). 
            Omit to return adjustments with any date.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: query
          name: patient
          schema:
            type: string
            format: uuid
          description: >-
            Filter adjustments to a single patient by patient UUID.  Omit to
            return every adjustment across the practice.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: array
            items:
              type: string
              format: uuid
          description: >-
            Filter by provider UUID.  Repeat the parameter to include multiple
            providers (``?provider=<uid>&provider=<uid>``).
      tags:
        - Billing
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedAdjustmentList'
          description: ''
    post:
      operationId: practice_billing_adjustment_create
      description: |-
        Practice-wide adjustment list (api#185).

        Backs the Adjustments report (web#154).  Supports ``date_from`` /
        ``date_to`` (inclusive, practice TZ), repeated ``provider`` and
        ``adj_type`` params, and an optional single ``patient`` filter.
        POST creates an adjustment with ``patient`` provided in the request
        body (the URL is not patient-scoped).
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Billing
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PracticeAdjustmentWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PracticeAdjustmentWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PracticeAdjustmentWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PracticeAdjustmentWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/billing/payment/:
    get:
      operationId: practice_billing_payment_list
      description: >-
        Practice-wide payment list. GET all payments, POST to create (patient in
        body).
      parameters:
        - in: query
          name: date_from
          schema:
            type: string
            format: date
          description: Earliest ``pay_date`` to include (inclusive, YYYY-MM-DD).
        - in: query
          name: date_to
          schema:
            type: string
            format: date
          description: Latest ``pay_date`` to include (inclusive, YYYY-MM-DD).
        - in: query
          name: method
          schema:
            type: array
            items:
              type: string
          description: >-
            Filter by payment-method bucket.  Repeatable.  One of
            ``cash``/``check``/``card``/``insurance``/``other``; the server maps
            the bucket to the set of raw PMS pay_type strings (``visa``, ``mc``,
            ``carecredit`` etc.).
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: query
          name: patient
          schema:
            type: string
            format: uuid
          description: >-
            Filter payments to a single patient by patient UUID.  Omit to return
            every payment across the practice.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: array
            items:
              type: string
              format: uuid
          description: >-
            Filter by provider UUID.  Repeatable — e.g.
            ``?provider=<a>&provider=<b>`` narrows to payments booked against
            either provider.
      tags:
        - Billing
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPaymentRecordList'
          description: ''
    post:
      operationId: practice_billing_payment_create
      description: >-
        Practice-wide payment list. GET all payments, POST to create (patient in
        body).
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Billing
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PracticePaymentWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PracticePaymentWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PracticePaymentWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PracticePaymentWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/billing/reports/aging/:
    get:
      operationId: practice_billing_reports_aging_retrieve
      description: >-
        A/R aging report — practice-wide summary, top 25 balances, plus
        ``by_provider`` and ``by_insurance`` breakdowns (api#303).  Per-row
        bucket totals in the breakdowns reconcile with the summary because both
        reuse the same FIFO-allocated per-patient numbers.
      parameters:
        - in: query
          name: as_of_date
          schema:
            type: string
            format: date
          description: >-
            Reference date for age calculation (YYYY-MM-DD, practice-local). 
            Defaults to today.  ``as_of`` is accepted as an alias.
        - in: query
          name: include_archived
          schema:
            type: boolean
          description: >-
            When ``true``, include inactive patients in the aggregation. 
            Defaults to ``false`` (active only).
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Billing
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgingReportResponse'
          description: ''
  /api/v1/practice/{practice_uid}/billing/reports/aging/jobs/:
    get:
      operationId: practice_billing_reports_aging_jobs_retrieve
      description: Poll for an async aging report result.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Billing
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
    post:
      operationId: practice_billing_reports_aging_jobs_create
      description: Kick off an async aging report computation.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Billing
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/billing/reports/aging/jobs/{as_of_date}/:
    get:
      operationId: practice_billing_reports_aging_jobs_retrieve_2
      description: Poll for an async aging report result.
      parameters:
        - in: path
          name: as_of_date
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Billing
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
    post:
      operationId: practice_billing_reports_aging_jobs_create_2
      description: Kick off an async aging report computation.
      parameters:
        - in: path
          name: as_of_date
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Billing
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/billing/reports/patient-balance/:
    get:
      operationId: practice_billing_reports_patient_balance_retrieve
      description: |-
        GET practice-wide patient-balance report with aging buckets (api#152).

        Backs the Patient Balance report UI (web#128).  Returns one paginated
        row per patient with a non-trivial balance, each row carrying the
        four aging-bucket amounts (0-30 / 31-60 / 61-90 / over-90), the
        computed ``balance_total``, last-payment metadata, and enough patient
        info for the table to render without a second round-trip.

        Aging buckets are computed from live ``Procedure`` / ``PaymentRecord``
        / ``Adjustment`` / ``InsuranceClaim`` data (not the stored
        :class:`PatientBalance` snapshot) so the ``as_of`` parameter is
        honored.  Buckets start as gross completed charges in each age range
        and are then reduced oldest-first by the net of payments,
        adjustments, and insurance-paid amounts so the summed buckets always
        reconcile with ``balance_total`` — the convention every PMS aging
        report uses.

        Query params:
            as_of        (YYYY-MM-DD, default today) — reference date for ages
            bucket       (0_30 | 31_60 | 61_90 | over_90) — post-allocation filter
            min_balance  (decimal, default 0.01) — drops zero / credit balances
            provider     (UUID) — restrict to patients with this primary provider
            carrier      (UUID) — restrict to patients whose primary insurance
                         ``PatientInsurance`` (ordinal=1) points at this carrier;
                         malformed UUIDs return 400. Backs the A/R aging
                         drill-down (web#327) for scoping by carrier.
            status       (active | inactive | any, default active)
      parameters:
        - in: query
          name: as_of
          schema:
            type: string
            format: date
          description: >-
            Reference date for age calculation (YYYY-MM-DD, practice TZ).
            Defaults to today.
        - in: query
          name: bucket
          schema:
            type: string
          description: >-
            Filter to a single aging bucket after allocation.  One of ``0_30`` /
            ``31_60`` / ``61_90`` / ``over_90``.  Returns only patients with a
            positive net balance in the named bucket.
        - in: query
          name: carrier
          schema:
            type: string
            format: uuid
          description: >-
            Restrict to patients whose primary insurance
            (``PatientInsurance.ordinal=1``) points at this ``InsuranceCarrier``
            UUID.  Malformed UUID returns 400.  Backs the A/R aging drill-down
            (web#327).
        - in: query
          name: min_balance
          schema:
            type: number
            format: double
          description: >-
            Exclude rows whose ``balance_total`` is below this threshold.
            Defaults to 0.01.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: string
            format: uuid
          description: Restrict to patients whose ``primary_provider`` matches this UUID.
        - in: query
          name: status
          schema:
            type: string
          description: >-
            Patient status filter: ``active`` (default) / ``inactive`` /
            ``any``.
      tags:
        - Billing
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientBalanceReportResponse'
          description: ''
  /api/v1/practice/{practice_uid}/billing/reports/production/:
    get:
      operationId: practice_billing_reports_production_retrieve
      description: |-
        GET production report aggregated across a date range.

        Practice-scoped (not patient-scoped).  Returns total production,
        collections, adjustments, net production, and procedure count — both as
        a summary and broken down by provider.  Also exposes a per-procedure
        rollup (``by_procedure``) and splits ``scheduled_production`` vs
        ``completed_production`` on each ``by_provider`` row so the Production
        Report UI (web#151) can surface pipeline work alongside collected work.

        Procedures tied to cancelled appointments are pulled out of every
        bucket — the same rule as the trending report in
        :mod:`apps.billing.reports`.

        Query params:
            date_from (YYYY-MM-DD) — defaults to 30 days ago
            date_to   (YYYY-MM-DD) — defaults to today
            provider  (UUID, repeatable) — narrows the summary totals and the
                      ``by_provider`` / ``by_procedure`` breakdowns; null-provider
                      rows are excluded when the filter is active.
      parameters:
        - in: query
          name: date_from
          schema:
            type: string
            format: date
          description: Start date (YYYY-MM-DD)
          required: true
        - in: query
          name: date_to
          schema:
            type: string
            format: date
          description: End date (YYYY-MM-DD)
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: array
            items:
              type: string
              format: uuid
          description: >-
            Filter to these providers only.  Repeatable
            (``?provider=<a>&provider=<b>``).  Narrows the summary totals and
            the ``by_provider`` / ``by_procedure`` breakdowns.
      tags:
        - Billing
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductionReportResponse'
          description: ''
  /api/v1/practice/{practice_uid}/billing/reports/production-trending/:
    get:
      operationId: practice_billing_reports_production_trending_retrieve
      description: |-
        GET bucketed production-trending report (api#157).

        Backs the Production Trending report UI (web#137).  Returns one row
        per bucket (day / week / month) between ``from`` and ``to``, with
        totals summed across the range.

        Query params:
            from     (YYYY-MM-DD, required)
            to       (YYYY-MM-DD, required)
            bucket   (day | week | month, optional — defaults by range size)
            provider (UUID, repeatable — narrows to listed providers)
            metric   (production,collections,adjustments,net_production —
                      comma-separated; controls which amounts appear in
                      each bucket row.  ``appointment_count`` is always
                      returned.)

        The single-range ``/reports/production/`` endpoint is preserved for
        backwards compatibility; this endpoint replaces its call site only
        when the caller wants a time-series breakdown.
      parameters:
        - in: query
          name: bucket
          schema:
            type: string
          description: >-
            Bucket granularity: ``day`` / ``week`` / ``month``.  Omit to let the
            server pick based on range size (> 90 days → month, > 14 days →
            week, else day).
        - in: query
          name: from
          schema:
            type: string
            format: date
          description: Start of the report range (inclusive, YYYY-MM-DD, practice TZ).
          required: true
        - in: query
          name: metric
          schema:
            type: string
          description: >-
            Comma-separated list of amount fields to include in each bucket
            row.  Valid values: ``production``, ``collections``,
            ``adjustments``, ``net_production``.  Omit to include all four. 
            ``appointment_count`` is always returned.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: array
            items:
              type: string
              format: uuid
          description: >-
            Filter to these providers only.  Repeatable
            (``?provider=<a>&provider=<b>``).
        - in: query
          name: to
          schema:
            type: string
            format: date
          description: End of the report range (inclusive, YYYY-MM-DD, practice TZ).
          required: true
      tags:
        - Billing
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductionTrendingResponse'
          description: ''
  /api/v1/practice/{practice_uid}/clinical/alerts/:
    get:
      operationId: practice_clinical_alerts_retrieve
      description: |-
        Batch medical alert summaries for multiple patients.

        GET /practice/<uid>/clinical/alerts/?patient_ids=1,2,3
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/practice/{practice_uid}/clinical/auto-notes/:
    get:
      operationId: practice_clinical_auto_note_list
      description: >-
        Returns the practice's catalog of operator-built chairside macros
        (``#periomax`` -> a perio-eval template) for the chart-note editor's
        autocomplete surface.  Paginated with
        :class:`config.pagination.DefaultPageNumberPagination` (stable ordering
        via ``pk`` tiebreaker).
      summary: List chairside auto-note macros for a practice.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: search
          schema:
            type: string
          description: Case-insensitive substring search across ``name`` and ``body``.
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedAutoNoteList'
          description: ''
    post:
      operationId: practice_clinical_auto_note_create
      description: >-
        Creates a new auto-note macro, or updates the existing row when
        ``(practice, name)`` already exists.  This is the idempotent landing
        path for the Eaglesoft / OpenDental converters (``escape-tools#205``,
        ``escape-tools#206``): re-running an import will refresh the body /
        prompts instead of returning HTTP 400 from the unique constraint.


        Returns ``201 Created`` for new rows and ``200 OK`` for updates.
      summary: Create or upsert an auto-note macro.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutoNoteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AutoNoteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/AutoNoteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutoNote'
          description: ''
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutoNote'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/clinical/auto-notes/{auto_note_uid}/:
    get:
      operationId: practice_clinical_auto_note_retrieve
      description: |-
        Retrieve, update, or delete a single :class:`AutoNote`.

        Hard delete is correct here -- auto-notes are authoring aids, not
        patient data, so HIPAA immutability does not apply.
      parameters:
        - in: path
          name: auto_note_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutoNote'
          description: ''
    put:
      operationId: practice_clinical_auto_notes_update
      description: |-
        Retrieve, update, or delete a single :class:`AutoNote`.

        Hard delete is correct here -- auto-notes are authoring aids, not
        patient data, so HIPAA immutability does not apply.
      parameters:
        - in: path
          name: auto_note_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutoNoteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AutoNoteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/AutoNoteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutoNote'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_clinical_auto_note_partial_update
      description: |-
        Retrieve, update, or delete a single :class:`AutoNote`.

        Hard delete is correct here -- auto-notes are authoring aids, not
        patient data, so HIPAA immutability does not apply.
      parameters:
        - in: path
          name: auto_note_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedAutoNoteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedAutoNoteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedAutoNoteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutoNote'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_clinical_auto_note_destroy
      description: |-
        Retrieve, update, or delete a single :class:`AutoNote`.

        Hard delete is correct here -- auto-notes are authoring aids, not
        patient data, so HIPAA immutability does not apply.
      parameters:
        - in: path
          name: auto_note_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/clinical/note-template/:
    get:
      operationId: practice_clinical_note_template_list
      description: |-
        List and create clinical note templates for a practice.

        Supports filtering by category and active status.

        Uses :class:`ReferenceDataPagination` (100/2000) instead of the default
        20/100 cap because the web UI loads the full note-template catalog once
        for typeahead / dropdown rendering inside the chart.  A mature practice
        accumulates hundreds of templates across categories -- paginating 20 at
        a time would thrash the chart UI.  See api#194.
      parameters:
        - in: query
          name: active
          schema:
            type: string
            enum:
              - 'false'
              - 'true'
          description: Filter by active status. Accepts ``true`` or ``false``.
        - in: query
          name: category
          schema:
            type: string
            enum:
              - endo
              - exam
              - general
              - hygiene
              - oral_surgery
          description: Filter by template category.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedNoteTemplateList'
          description: ''
    post:
      operationId: practice_clinical_note_template_create
      description: |-
        List and create clinical note templates for a practice.

        Supports filtering by category and active status.

        Uses :class:`ReferenceDataPagination` (100/2000) instead of the default
        20/100 cap because the web UI loads the full note-template catalog once
        for typeahead / dropdown rendering inside the chart.  A mature practice
        accumulates hundreds of templates across categories -- paginating 20 at
        a time would thrash the chart UI.  See api#194.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NoteTemplateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/NoteTemplateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/NoteTemplateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NoteTemplate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/clinical/note-template/{note_template_uid}/:
    get:
      operationId: practice_clinical_note_template_retrieve
      description: Retrieve, update, or delete a clinical note template.
      parameters:
        - in: path
          name: note_template_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NoteTemplate'
          description: ''
    put:
      operationId: practice_clinical_note_template_update
      description: Retrieve, update, or delete a clinical note template.
      parameters:
        - in: path
          name: note_template_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NoteTemplateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/NoteTemplateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/NoteTemplateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NoteTemplate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_clinical_note_template_partial_update
      description: Retrieve, update, or delete a clinical note template.
      parameters:
        - in: path
          name: note_template_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedNoteTemplateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedNoteTemplateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedNoteTemplateRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NoteTemplate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_clinical_note_template_destroy
      description: Retrieve, update, or delete a clinical note template.
      parameters:
        - in: path
          name: note_template_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/clinical/procedure/:
    get:
      operationId: practice_clinical_procedure_list
      description: >-
        Practice-wide procedure list and bulk create.


        GET returns all procedures for the practice (paginated, filterable).

        POST accepts a JSON array for bulk import (up to 5000 records).


        Used by escape-opendental, escape-eaglesoft, and escape-backup tools.


        Uses PageNumberPagination instead of the default CursorPagination
        because

        bulk-imported procedures may have NULL ``created`` timestamps, which
        causes

        CursorPagination to crash with a 500.
      parameters:
        - in: query
          name: date_from
          schema:
            type: string
            format: date
          description: Inclusive lower bound on ``proc_date`` (ISO 8601 date).
        - in: query
          name: date_to
          schema:
            type: string
            format: date
          description: Inclusive upper bound on ``proc_date`` (ISO 8601 date).
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: query
          name: patient
          schema:
            type: string
            format: uuid
          description: Filter to procedures for a single patient UUID.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: string
            format: uuid
          description: Filter by provider UUID.
        - in: query
          name: status
          schema:
            type: string
            enum:
              - complete
              - existing_current
              - existing_other
          description: >-
            Filter by procedure status. Common values are ``complete``,
            ``existing_current``, and ``existing_other``; free-text is accepted
            for legacy PMS imports.
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedProcedureList'
          description: ''
    post:
      operationId: practice_clinical_procedure_create
      description: >-
        Practice-wide procedure list and bulk create.


        GET returns all procedures for the practice (paginated, filterable).

        POST accepts a JSON array for bulk import (up to 5000 records).


        Used by escape-opendental, escape-eaglesoft, and escape-backup tools.


        Uses PageNumberPagination instead of the default CursorPagination
        because

        bulk-imported procedures may have NULL ``created`` timestamps, which
        causes

        CursorPagination to crash with a 500.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProcedureBulkWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ProcedureBulkWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ProcedureBulkWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProcedureBulkWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_clinical_procedure_partial_update
      description: Handle PATCH on the list endpoint for bulk updates.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedProcedureRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedProcedureRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedProcedureRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Procedure'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/clinical/procedure-code/:
    get:
      operationId: practice_clinical_procedure_code_list
      description: >-
        List and create procedure codes for a practice. Searchable by
        code/description.


        Uses :class:`ReferenceDataPagination` (100/2000) instead of the default

        20/100 cap because the web UI loads the full catalog once for local

        typeahead and service-type filtering across dropdowns.  A typical

        practice catalog is 1K-4K ADA codes -- paginating 20 at a time would

        require dozens of round-trips and break the Service Codes screen /

        checkout dialog procedure search.
      parameters:
        - in: query
          name: active
          schema:
            type: string
            enum:
              - 'false'
              - 'true'
          description: Filter by active status. Accepts ``true`` or ``false``.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: search
          schema:
            type: string
          description: >-
            Case-insensitive substring search across ``code``, ``description``,
            and ``abbreviation``.
        - in: query
          name: service_type
          schema:
            type: string
            enum:
              - adjunctive
              - basic
              - diagnostic
              - endodontics
              - major
              - oral_surgery
              - orthodontics
              - other
              - periodontics
              - preventive
              - prosthodontics
          description: Filter by ADA service type classification.
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedProcedureCodeList'
          description: ''
    post:
      operationId: practice_clinical_procedure_code_create
      description: >-
        List and create procedure codes for a practice. Searchable by
        code/description.


        Uses :class:`ReferenceDataPagination` (100/2000) instead of the default

        20/100 cap because the web UI loads the full catalog once for local

        typeahead and service-type filtering across dropdowns.  A typical

        practice catalog is 1K-4K ADA codes -- paginating 20 at a time would

        require dozens of round-trips and break the Service Codes screen /

        checkout dialog procedure search.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProcedureCodeRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ProcedureCodeRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ProcedureCodeRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProcedureCode'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_clinical_procedure_code_bulk_partial_update
      description: Handle PATCH on the list endpoint for bulk updates.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedProcedureCodeRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedProcedureCodeRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedProcedureCodeRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProcedureCode'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/clinical/procedure-code/{procedure_code_uid}/:
    get:
      operationId: practice_clinical_procedure_code_retrieve
      description: Retrieve or update a procedure code.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: procedure_code_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProcedureCode'
          description: ''
    put:
      operationId: practice_clinical_procedure_code_update
      description: Retrieve or update a procedure code.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: procedure_code_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProcedureCodeRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ProcedureCodeRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ProcedureCodeRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProcedureCode'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_clinical_procedure_code_partial_update
      description: Retrieve or update a procedure code.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: procedure_code_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedProcedureCodeRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedProcedureCodeRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedProcedureCodeRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProcedureCode'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/clinical/procedure-code/suggest/:
    get:
      operationId: practice_clinical_procedure_code_suggest_retrieve
      description: >-
        Suggest an appropriate CDT code based on patient clinical history.


        GET /practice/<uid>/clinical/procedure-code/suggest/?patient_id=<id>


        Checks if the patient has any completed D4341/D4342 (SRP) procedures.

        If yes, suggests D4910 (perio maintenance) instead of D1110
        (prophylaxis).
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: query
          schema:
            type: string
          description: Search query for procedure code suggestions
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/practice/{practice_uid}/comms/commlog/:
    get:
      operationId: practice_comms_commlog_list
      description: |-
        Practice-wide commlog list and bulk create.

        GET returns all commlogs for the practice (paginated, filterable by
        ``patient``, ``date_from``, ``date_to``).
        POST accepts a JSON array for bulk import (up to 5000 records).
        PATCH accepts a JSON array of ``{id, ...}`` objects for bulk update.

        Used by escape-opendental and escape-eaglesoft sync tools to avoid
        per-patient fetch loops.  Each record must include ``patient`` in the
        body since the endpoint is not patient-scoped.
      parameters:
        - in: query
          name: date_from
          schema:
            type: string
            format: date
          description: >-
            Inclusive lower bound on ``date_time`` (``YYYY-MM-DD``).  Filters
            against the practice-local date portion of the comm log's timestamp.
        - in: query
          name: date_to
          schema:
            type: string
            format: date
          description: >-
            Inclusive upper bound on ``date_time`` (``YYYY-MM-DD``).  Filters
            against the practice-local date portion of the comm log's timestamp.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: query
          name: patient
          schema:
            type: string
            format: uuid
          description: >-
            Filter comm logs to a single patient UID.  Prefer the patient-scoped
            endpoint (``/patient/{patient_id}/comms/commlog/``) when you already
            have the patient in hand; this practice-wide view exists for sync
            tools that pull across the whole practice in one query.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedCommLogList'
          description: ''
    post:
      operationId: practice_comms_commlog_create
      description: |-
        Practice-wide commlog list and bulk create.

        GET returns all commlogs for the practice (paginated, filterable by
        ``patient``, ``date_from``, ``date_to``).
        POST accepts a JSON array for bulk import (up to 5000 records).
        PATCH accepts a JSON array of ``{id, ...}`` objects for bulk update.

        Used by escape-opendental and escape-eaglesoft sync tools to avoid
        per-patient fetch loops.  Each record must include ``patient`` in the
        body since the endpoint is not patient-scoped.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommLogBulkWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/CommLogBulkWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/CommLogBulkWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommLogBulkWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_comms_commlog_partial_update
      description: Handle PATCH on the list endpoint for bulk updates.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedCommLogBulkWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedCommLogBulkWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedCommLogBulkWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommLogBulkWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/doccenter/capture-token/:
    post:
      operationId: practice_doccenter_capture_token_create
      description: |-
        Create a document capture session and return a one-time token.

        Called by the web app when staff clicks "Scan Documents". The token is
        passed to the desktop app via ``escape-documents://capture?token=...``.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/doccenter/category/:
    get:
      operationId: practice_doccenter_category_list
      description: >-
        List system + practice categories; POST creates a practice-custom
        category.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedDocumentCategoryList'
          description: ''
    post:
      operationId: practice_doccenter_category_create
      description: >-
        List system + practice categories; POST creates a practice-custom
        category.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentCategoryRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/DocumentCategoryRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/DocumentCategoryRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentCategory'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/doccenter/category/{category_uid}/:
    get:
      operationId: practice_doccenter_category_retrieve
      description: Update/delete practice-custom categories only.
      parameters:
        - in: path
          name: category_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentCategory'
          description: ''
    put:
      operationId: practice_doccenter_category_update
      description: Update/delete practice-custom categories only.
      parameters:
        - in: path
          name: category_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentCategoryRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/DocumentCategoryRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/DocumentCategoryRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentCategory'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_doccenter_category_partial_update
      description: Update/delete practice-custom categories only.
      parameters:
        - in: path
          name: category_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedDocumentCategoryRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedDocumentCategoryRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedDocumentCategoryRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentCategory'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_doccenter_category_destroy
      description: Update/delete practice-custom categories only.
      parameters:
        - in: path
          name: category_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/doccenter/document/:
    get:
      operationId: practice_doccenter_document_list
      description: List practice-level documents (no patient). Filter by ?category=.
      parameters:
        - in: query
          name: category
          schema:
            type: string
            format: uuid
          description: >-
            Filter by document category UID.  Categories are returned by ``GET
            /api/v1/practice/{practice_uid}/doccenter/category/`` and include
            both system (``practice is null``) and practice-custom entries.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedDocumentList'
          description: ''
  /api/v1/practice/{practice_uid}/doccenter/document/{doc_id}/:
    get:
      operationId: practice_doccenter_document_retrieve
      description: Retrieve or update document metadata.
      parameters:
        - in: path
          name: doc_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Document'
          description: ''
    put:
      operationId: practice_doccenter_document_update
      description: Retrieve or update document metadata.
      parameters:
        - in: path
          name: doc_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentUpdateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/DocumentUpdateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/DocumentUpdateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentUpdate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_doccenter_document_partial_update
      description: Retrieve or update document metadata.
      parameters:
        - in: path
          name: doc_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedDocumentUpdateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedDocumentUpdateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedDocumentUpdateRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentUpdate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/doccenter/document/{doc_id}/delete/:
    delete:
      operationId: practice_doccenter_document_delete_destroy
      description: Soft-delete a document (HIPAA retention).
      parameters:
        - in: path
          name: doc_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/doccenter/document/search/:
    get:
      operationId: practice_doccenter_document_search_retrieve
      description: >-
        Full-text search across document titles, descriptions, filenames, and
        OCR-extracted body text within a practice.  Results are ranked by
        Elasticsearch relevance score and include highlighted snippets around
        matched terms.


        **HIPAA scoping** — results are always restricted to the practice in the
        URL.  Cross-practice leakage is a critical bug; the practice filter is
        enforced at the Elasticsearch query layer and cannot be bypassed via
        query parameters.


        Pass ``patient_id`` to scope to a single patient (patient docs tab), or
        omit it for practice-wide search.  Pass ``category`` (slug) to narrow to
        one document category.
      summary: Search documents by full-text content with highlights
      parameters:
        - in: query
          name: category
          schema:
            type: string
          description: >-
            Optional category slug (e.g. ``insurance``, ``referrals``). 
            Resolved against system + practice-custom categories.
        - in: query
          name: page
          schema:
            type: integer
          description: 1-based page number for pagination.
        - in: query
          name: page_size
          schema:
            type: integer
          description: Page size (max 100, default 20).
        - in: query
          name: patient_id
          schema:
            type: string
            format: uuid
          description: Optional patient UID — narrows to one patient's documents.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: q
          schema:
            type: string
          description: Search query.  Required.  Must be at least 2 characters.
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentSearchResponse'
          description: ''
        '400':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
        '503':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
  /api/v1/practice/{practice_uid}/doccenter/document/upload/:
    post:
      operationId: practice_doccenter_document_upload_create
      description: Upload a practice-level document.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/doccenter/inbox/:
    get:
      operationId: practice_doccenter_inbox_list
      description: |-
        List inbox items for this practice.

        Practice Inbox (escape#151) renders three tabs — Documents (emailed in),
        Voicemails, and Faxes — each backed by a single-source list query.  The
        ``?source=`` filter lets the web client pull just the rows it needs per
        tab without round-tripping every pending item and filtering client-side.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedInboxItemList'
          description: ''
  /api/v1/practice/{practice_uid}/doccenter/inbox/{inbox_uid}/:
    get:
      operationId: practice_doccenter_inbox_retrieve
      description: View or update inbox item (title, notes).
      parameters:
        - in: path
          name: inbox_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InboxItem'
          description: ''
    put:
      operationId: practice_doccenter_inbox_update
      description: View or update inbox item (title, notes).
      parameters:
        - in: path
          name: inbox_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InboxItemRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/InboxItemRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/InboxItemRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InboxItem'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_doccenter_inbox_partial_update
      description: View or update inbox item (title, notes).
      parameters:
        - in: path
          name: inbox_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedInboxItemRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedInboxItemRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedInboxItemRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InboxItem'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/doccenter/inbox/{inbox_uid}/accept/:
    post:
      operationId: practice_doccenter_inbox_accept_create
      description: Convert an inbox item to a permanent Document.
      parameters:
        - in: path
          name: inbox_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/doccenter/inbox/{inbox_uid}/reject/:
    delete:
      operationId: practice_doccenter_inbox_reject_destroy
      description: Hard-delete an inbox item and its S3 file.
      parameters:
        - in: path
          name: inbox_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/doccenter/inbox/upload/:
    post:
      operationId: practice_doccenter_inbox_upload_create
      description: Upload a file to the inbox.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/doccenter/scan-to-email/:
    get:
      operationId: practice_doccenter_scan_to_email_retrieve
      description: 'GET: return config. POST: enable scan-to-email. DELETE: disable.'
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
    post:
      operationId: practice_doccenter_scan_to_email_create
      description: 'GET: return config. POST: enable scan-to-email. DELETE: disable.'
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_doccenter_scan_to_email_destroy
      description: 'GET: return config. POST: enable scan-to-email. DELETE: disable.'
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/doccenter/voicemail-inbox/:
    get:
      operationId: practice_doccenter_voicemail_inbox_list
      description: >-
        Returns voicemail recordings (``Document`` rows under the system
        ``voicemail`` category) for the practice, ranked with caller-ID patient
        suggestions.  Default scope is ``unlinked`` — voicemails with ``patient
        is None``.  Operators confirm a suggestion via ``PATCH
        /doccenter/document/<doc_id>/`` setting ``patient`` (the existing
        audit-emitting Document update endpoint).
      summary: Voicemail inbox — un-linked voicemails with patient suggestions
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: status
          schema:
            type: string
            enum:
              - all
              - linked
              - unlinked
          description: >-
            Filter by link state: ``unlinked`` (default — no patient yet),
            ``linked`` (already linked to a patient), or ``all``.
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedVoicemailInboxRowList'
          description: ''
  /api/v1/practice/{practice_uid}/doccenter/voicemail/{doc_id}/resolve/:
    post:
      operationId: practice_doccenter_voicemail_resolve_create
      description: >-
        Marks the voicemail Document as resolved against the practice — it drops
        out of ``GET /doccenter/voicemail-inbox/`` (default ``status=unlinked``)
        without being deleted.  Idempotent: re-posting on an already-resolved
        row refreshes the actor + timestamp.  Reversible via ``DELETE``.
      summary: File a voicemail against the practice (resolve without a patient)
      parameters:
        - in: path
          name: doc_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Document'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
    delete:
      operationId: practice_doccenter_voicemail_resolve_destroy
      description: >-
        Clears the ``resolved_at`` flag, returning the voicemail to the default
        unlinked queue.  Used by the triage UI's undo affordance.
      summary: Un-resolve a voicemail filed against the practice
      parameters:
        - in: path
          name: doc_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Document'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
  /api/v1/practice/{practice_uid}/fax/number/:
    get:
      operationId: practice_fax_number_retrieve
      description: |-
        Read the practice's active fax number (api#550).

        ``GET /api/v1/practice/<uid>/fax/number/`` returns the active
        :class:`PracticeFaxNumber` row, or 404 when none has been provisioned.

        Open to any practice staff -- the cheat-sheet popover in the staff
        header surfaces the number for everyone, not just admins.  Provision
        (``POST /fax/provision/``) stays admin-only.

        The previous read path was sniffing the latest fax message in the
        inbox for a ``from_number`` (``useActiveFaxNumber`` on the web side),
        which only worked after the first fax flowed through.  A
        freshly-provisioned number is invisible to that path until somebody
        sends or receives one.  This endpoint reads the actual provisioning
        row instead.
      summary: Get the practice's active fax number
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PracticeFaxNumber'
          description: ''
        '404':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
  /api/v1/practice/{practice_uid}/fax/provision/:
    post:
      operationId: practice_fax_provision_create
      description: |-
        Provision a fax number for the practice (api#531).

        ``POST /api/v1/practice/<uid>/fax/provision/`` searches the upstream
        number inventory, picks the first candidate, rents it, and persists a
        :class:`PracticeFaxNumber` row.  The flow is admin-only and
        idempotent on a per-practice basis -- a practice that already has an
        active number gets a 409 with the existing serialized row.

        Per-number cost is absorbed into the base subscription, so
        provisioning is **not** plan-gated: any practice can rent a number
        through this endpoint.  The send-side ``includes_fax`` gate
        (GH-443) still applies to actually transmitting documents, which is
        the per-page metered cost.

        Body (all fields optional)::

            {
                "area_code": "541",
                "preferred_locality": "OR"
            }

        ``area_code`` overrides the prefix sent upstream as
        ``numberPattern.searchPattern``.  ``preferred_locality`` is a hint
        today (we forward it as the regionCode-narrowing context for future
        ``cityName`` support).  When neither is supplied we infer from
        the practice's address, falling back through
        ``zip_code -> state -> any-US`` until the upstream returns a result.
      summary: Provision a fax number for the practice
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FaxProvisionRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/FaxProvisionRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/FaxProvisionRequestRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PracticeFaxNumber'
          description: ''
        '400':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '409':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PracticeFaxNumber'
          description: ''
        '502':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
  /api/v1/practice/{practice_uid}/faxes/:
    get:
      operationId: practice_faxes_list
      description: |-
        List and create faxes for the practice.

        GET is read-only and open to any practice staff; POST is admin-only
        and gated by the practice's plan tier.  The view sets ``practice``
        and ``sent_by`` server-side; the serializer never trusts those from
        the body.
      parameters:
        - in: query
          name: delivery_status
          schema:
            type: array
            items:
              type: string
              enum:
                - delivered
                - failed
                - queued
                - receiving
                - sending
                - sent
          description: >-
            Filter by per-fax delivery state (the ``status`` column on the row,
            distinct from the ``?status=`` triage filter).  Accepts repeated
            params (``?delivery_status=delivered&delivery_status=failed``) or a
            comma-separated list (``?delivery_status=delivered,failed``); values
            within the filter are OR-combined.  Unknown values are dropped. 
            Primary use case: the Fax Inbox excluding ``receiving`` rows
            server-side so the paginated ``count`` envelope matches what the
            user sees (GH-611).
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: status
          schema:
            type: string
            enum:
              - all
              - discarded
              - filed
              - unfiled
          description: >-
            Filter by triage state.  ``unfiled`` returns inbound faxes still
            pending triage (no patient, no practice link, not discarded) — used
            by the Practice Inbox.  ``filed`` and ``discarded`` are convenience
            filters for focused review.  ``all`` (default) returns every fax —
            outbound rows always appear under ``all`` since filing only applies
            to inbound.
      tags:
        - Communications
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedFaxList'
          description: ''
    post:
      operationId: practice_faxes_create
      description: |-
        List and create faxes for the practice.

        GET is read-only and open to any practice staff; POST is admin-only
        and gated by the practice's plan tier.  The view sets ``practice``
        and ``sent_by`` server-side; the serializer never trusts those from
        the body.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      requestBody:
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/FaxCreateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FaxCreate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/faxes/{fax_id}/:
    get:
      operationId: practice_faxes_retrieve
      description: |-
        Retrieve a single fax row, including a signed download URL.

        Read-only — there's no legitimate reason to PATCH a fax row in
        place.  Status changes flow through the webhook handler, not the
        user-facing API.
      parameters:
        - in: path
          name: fax_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Fax'
          description: ''
  /api/v1/practice/{practice_uid}/faxes/{fax_id}/discard/:
    post:
      operationId: practice_faxes_discard_create
      description: >-
        Discard / un-discard an inbound fax (api#508).


        Soft-flag — the underlying PDF is preserved for HIPAA retention and the

        row remains queryable for audit.  DELETE marks the fax discarded; POST
        on

        the same path restores it (returns it to the unfiled queue).
      summary: Restore a discarded fax (returns it to the unfiled queue)
      parameters:
        - in: path
          name: fax_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Fax'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
    delete:
      operationId: practice_faxes_discard_destroy
      description: >-
        Discard / un-discard an inbound fax (api#508).


        Soft-flag — the underlying PDF is preserved for HIPAA retention and the

        row remains queryable for audit.  DELETE marks the fax discarded; POST
        on

        the same path restores it (returns it to the unfiled queue).
      summary: Discard an inbound fax (soft-flag — preserves PDF)
      parameters:
        - in: path
          name: fax_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '400':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
  /api/v1/practice/{practice_uid}/faxes/{fax_id}/file/:
    post:
      operationId: practice_faxes_file_create
      description: >-
        File an inbound fax against a patient or against the practice (api#508).


        POST body:
          * ``{"patient": "<uid>"}`` → file against the named patient.
          * ``{}`` (or ``{"patient": null}``) → file against the practice.

        Idempotent: re-filing refreshes ``filed_at`` and ``filed_by``. 
        Re-filing

        against a different target is allowed — staff who initially filed
        against

        the wrong patient can correct the assignment without an undo.


        Outbound faxes are not eligible (they file themselves implicitly via the

        staff workflow that originated them); attempting to file one returns
        400.

        Discarded faxes are not eligible either — restore via the discard

        endpoint's DELETE first.
      summary: File an inbound fax against a patient or against the practice
      parameters:
        - in: path
          name: fax_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FaxFileRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/FaxFileRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/FaxFileRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Fax'
          description: ''
        '400':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
  /api/v1/practice/{practice_uid}/forms/bundles/:
    get:
      operationId: practice_forms_bundles_list
      description: |-
        List or create form bundles.

        ``GET`` is open to any authenticated staff (no role check). ``POST``
        is gated to manager-or-above via :class:`IsManagerOrAbove` — same
        write-side guardrail the spec for api#769 requires.  The
        ``IsManagerOrAbove`` permission allows safe methods through
        unconditionally, so a single ``permission_classes`` list covers both
        cases without a per-method override.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedFormBundleList'
          description: ''
    post:
      operationId: practice_forms_bundles_create
      description: |-
        List or create form bundles.

        ``GET`` is open to any authenticated staff (no role check). ``POST``
        is gated to manager-or-above via :class:`IsManagerOrAbove` — same
        write-side guardrail the spec for api#769 requires.  The
        ``IsManagerOrAbove`` permission allows safe methods through
        unconditionally, so a single ``permission_classes`` list covers both
        cases without a per-method override.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FormBundleRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/FormBundleRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/FormBundleRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormBundle'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/forms/bundles/{bundle_id}/:
    get:
      operationId: practice_forms_bundles_retrieve
      description: |-
        Retrieve, update, or delete a form bundle.

        Update + delete are gated to manager-or-above (api#769); retrieve
        stays open to any authenticated staff thanks to
        :class:`IsManagerOrAbove` letting safe methods through.

        Delete cascades to :class:`FormBundleItem` rows (the FK is CASCADE)
        but PROTECTs against orphaning a :class:`FormSendOut` that still
        references the bundle — staff must archive the sendout first.  The
        PROTECT raises :class:`django.db.models.ProtectedError`, which DRF's
        default exception handler maps to 409 Conflict.
      parameters:
        - in: path
          name: bundle_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormBundle'
          description: ''
    patch:
      operationId: practice_forms_bundles_partial_update
      description: |-
        Retrieve, update, or delete a form bundle.

        Update + delete are gated to manager-or-above (api#769); retrieve
        stays open to any authenticated staff thanks to
        :class:`IsManagerOrAbove` letting safe methods through.

        Delete cascades to :class:`FormBundleItem` rows (the FK is CASCADE)
        but PROTECTs against orphaning a :class:`FormSendOut` that still
        references the bundle — staff must archive the sendout first.  The
        PROTECT raises :class:`django.db.models.ProtectedError`, which DRF's
        default exception handler maps to 409 Conflict.
      parameters:
        - in: path
          name: bundle_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedFormBundleRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedFormBundleRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedFormBundleRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormBundle'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_forms_bundles_destroy
      description: |-
        Retrieve, update, or delete a form bundle.

        Update + delete are gated to manager-or-above (api#769); retrieve
        stays open to any authenticated staff thanks to
        :class:`IsManagerOrAbove` letting safe methods through.

        Delete cascades to :class:`FormBundleItem` rows (the FK is CASCADE)
        but PROTECTs against orphaning a :class:`FormSendOut` that still
        references the bundle — staff must archive the sendout first.  The
        PROTECT raises :class:`django.db.models.ProtectedError`, which DRF's
        default exception handler maps to 409 Conflict.
      parameters:
        - in: path
          name: bundle_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/forms/bundles/default-new-patient/:
    get:
      operationId: practice_forms_bundles_default_new_patient_retrieve
      description: |-
        Return the practice's default new-patient bundle (api#769).

        Convenience endpoint for the staff web's "Send new patient forms"
        entry point — saves a list-then-filter round-trip.  Returns 404 if
        no bundle on this practice has ``is_default_new_patient=True``.

        Read access is open to any authenticated staff; there is no write
        surface on this URL — flipping the flag goes through
        :class:`FormBundleDetailView` PATCH.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormBundle'
          description: ''
  /api/v1/practice/{practice_uid}/forms/kiosk-sessions/:
    get:
      operationId: practice_forms_kiosk_sessions_list
      description: |-
        ``GET / POST /api/v1/practice/<uid>/forms/kiosk-sessions/``

        GET lists kiosk sessions for the practice (most-recent first).  POST
        mints a fresh device-bound session — body accepts ``device_label``
        and ``idle_timeout_seconds`` (both optional); ``token`` and
        ``kiosk_url`` come back in the response so the staff client can
        immediately point a tablet at the URL.

        Only one server-side endpoint creates kiosk rows; the public token
        path never produces them.  This keeps the create path firmly behind
        staff auth + the practice scope check.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedKioskSessionDetailList'
          description: ''
    post:
      operationId: practice_forms_kiosk_sessions_create
      description: |-
        ``GET / POST /api/v1/practice/<uid>/forms/kiosk-sessions/``

        GET lists kiosk sessions for the practice (most-recent first).  POST
        mints a fresh device-bound session — body accepts ``device_label``
        and ``idle_timeout_seconds`` (both optional); ``token`` and
        ``kiosk_url`` come back in the response so the staff client can
        immediately point a tablet at the URL.

        Only one server-side endpoint creates kiosk rows; the public token
        path never produces them.  This keeps the create path firmly behind
        staff auth + the practice scope check.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KioskSessionCreateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/KioskSessionCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/KioskSessionCreateRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KioskSessionCreate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/forms/kiosk-sessions/{session_id}/:
    get:
      operationId: practice_forms_kiosk_sessions_retrieve
      description: |-
        ``GET /api/v1/practice/<uid>/forms/kiosk-sessions/<id>/``

        Staff-facing retrieve.  Mutations live on dedicated action endpoints
        (``/load-sendout/`` and ``/end/``) so each verb carries its own
        audit event and validation rules.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: session_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KioskSessionDetail'
          description: ''
  /api/v1/practice/{practice_uid}/forms/kiosk-sessions/{session_id}/end/:
    post:
      operationId: practice_forms_kiosk_sessions_end_create
      description: |-
        ``POST /api/v1/practice/<uid>/forms/kiosk-sessions/<id>/end/``

        Explicitly terminates a kiosk session.  Idempotent — re-ending a
        session that already ended is a 200 with the existing row, not a
        409 or 410 (the staff client may double-click the End button).
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: session_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/forms/kiosk-sessions/{session_id}/load-sendout/:
    post:
      operationId: practice_forms_kiosk_sessions_load_sendout_create
      description: |-
        ``POST /api/v1/practice/<uid>/forms/kiosk-sessions/<id>/load-sendout/``

        Assigns a :class:`FormSendOut` to the kiosk session so the device can
        pull it on the next public state read.  The sendout must belong to
        the same practice as the kiosk and must not already be ``submitted``
        or ``expired`` — those would render an empty form.

        Loading a new sendout on a session that already had one is allowed
        (and intentional: the kiosk flow is "patient finishes form, staff
        walks over and taps Load Next Form" — the same session can host a
        series of submissions for one patient).
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: session_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/forms/mapping-proposals/:
    get:
      operationId: practice_forms_mapping_proposals_list
      description: >-
        ``GET /api/v1/practice/<uid>/forms/mapping-proposals/``


        Filterable by ``?submission=<uuid>``,
        ``?status=<pending|approved|rejected>``,

        and ``?patient=<uuid>``.  Returns the same shape the detail endpoint

        serves, paginated by the project default.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedFormFieldMappingProposalList'
          description: ''
  /api/v1/practice/{practice_uid}/forms/mapping-proposals/{proposal_id}/:
    get:
      operationId: practice_forms_mapping_proposals_retrieve
      description: '``GET /api/v1/practice/<uid>/forms/mapping-proposals/<id>/``'
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: proposal_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormFieldMappingProposal'
          description: ''
  /api/v1/practice/{practice_uid}/forms/mapping-proposals/{proposal_id}/approve/:
    post:
      operationId: practice_forms_mapping_proposals_approve_create
      description: |-
        ``POST /api/v1/practice/<uid>/forms/mapping-proposals/<id>/approve/``

        Body: ``{"notes": "optional"}``.

        Idempotent — re-approving an already-approved proposal is a no-op
        that returns 200 with the existing row.  Approving a previously
        rejected proposal is rejected with 409 (Conflict): the staff member
        should file a new submission / sendout cycle if they want to revisit.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: proposal_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/forms/mapping-proposals/{proposal_id}/reject/:
    post:
      operationId: practice_forms_mapping_proposals_reject_create
      description: |-
        ``POST /api/v1/practice/<uid>/forms/mapping-proposals/<id>/reject/``

        Body: ``{"notes": "required"}``.

        A non-blank ``notes`` is required so the audit trail explains why the
        proposed change did not land.  Re-rejecting an already-rejected
        proposal is idempotent.  Rejecting a previously approved proposal is
        a 409 — once the patient record changed, it's the staff member's job
        to walk it back via the appropriate patient-edit endpoint.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: proposal_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/forms/mapping-proposals/bulk-approve/:
    post:
      operationId: practice_forms_mapping_proposals_bulk_approve_create
      description: |-
        ``POST /api/v1/practice/<uid>/forms/mapping-proposals/bulk-approve/``

        Body: ``{"ids": [<uuid>, ...], "notes": "optional"}``.

        All-or-nothing: a single validation failure (unknown target field,
        bad date format, etc.) rolls back the entire batch.  Already-approved
        proposals are no-ops inside the batch; already-rejected proposals
        abort the batch with 409 — matching the single-row endpoint's
        semantics.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/forms/sendouts/:
    get:
      operationId: practice_forms_sendouts_list
      description: >-
        List or create sendouts.


        Creating a sendout mints a fresh signed link the staff can hand to the

        patient; reads return the list filtered by ``?status=`` and
        ``?patient=``.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedFormSendOutListList'
          description: ''
    post:
      operationId: practice_forms_sendouts_create
      description: >-
        List or create sendouts.


        Creating a sendout mints a fresh signed link the staff can hand to the

        patient; reads return the list filtered by ``?status=`` and
        ``?patient=``.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FormSendOutCreateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/FormSendOutCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/FormSendOutCreateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormSendOutCreate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/forms/sendouts/{sendout_id}/:
    get:
      operationId: practice_forms_sendouts_retrieve
      description: |-
        ``GET /api/v1/practice/<uid>/forms/sendouts/<id>/`` (api#654).

        Practice-scoped retrieve.  Closes the gap that forced the web
        detail page (web#532 / web#535) to walk the paginated list endpoint
        to render a single row.  Cross-tenant access returns 404 -- the
        sendout's existence is not leaked to a foreign practice.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: sendout_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormSendOutList'
          description: ''
  /api/v1/practice/{practice_uid}/forms/sendouts/{sendout_id}/cancel/:
    post:
      operationId: practice_forms_sendouts_cancel_create
      description: |-
        ``POST /api/v1/practice/<uid>/forms/sendouts/<id>/cancel/`` (api#654).

        Flip ``status`` to :attr:`SendOutStatus.ARCHIVED` so the sendout
        no longer blocks template archive and the patient-side endpoint
        stops accepting submissions against the token.

        Status semantics
        ~~~~~~~~~~~~~~~~
        * ``pending`` / ``in_progress`` -> ``archived`` + audit event.
        * ``archived`` / ``expired`` -> 200 idempotent re-cancel; audit
          event still fires so the trail captures the intent regardless
          of state (mirrors the template-archive contract).
        * ``submitted`` -> 409.  A completed sendout is history; the
          patient already filled the form and the chart already carries
          the submission.  Cancelling here would lie to the audit trail
          about what really happened.

        The transition is a single-column UPDATE so a racing transition
        on the same row (e.g. the patient submitting between our
        ``select_for_update`` and the write) can't trample the
        ``modified`` timestamp on either side; we re-check the row
        inside the same transaction to keep the contract honest.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: sendout_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/forms/sendouts/{sendout_id}/resend/:
    post:
      operationId: practice_forms_sendouts_resend_create
      description: |-
        ``POST /api/v1/practice/<uid>/forms/sendouts/<id>/resend/`` (api#655).

        Re-broadcast the patient-facing notification for ``sendout_id`` using
        the SAME token (no new credential is minted -- staff explicitly asked
        for "send the same link again").  Throttled to at most one delivery
        per :data:`RESEND_THROTTLE` window per sendout so a button-mash does
        not spam the patient.

        Lifecycle gating
        ~~~~~~~~~~~~~~~~
        * ``pending`` / ``in_progress`` -- allowed.
        * ``submitted`` / ``expired`` / ``archived`` -- rejected with 409.
          A submitted form has nothing to remind about; an expired or
          cancelled token would land the patient on a 410 the moment they
          tap the link.

        Throttle contract
        ~~~~~~~~~~~~~~~~~
        Returns 429 with ``{"detail": "...", "next_resend_at": "<iso>"}``
        when the previous resend landed inside the window.  The web client
        uses ``next_resend_at`` to gray out the button until the deadline
        elapses without polling.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: sendout_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/forms/sendouts/{sendout_id}/submissions/:
    get:
      operationId: practice_forms_sendouts_submissions_list
      description: >-
        ``GET /api/v1/practice/<uid>/forms/sendouts/<id>/submissions/``
        (api#656).


        List :class:`FormSubmission` rows attached to ``sendout_id``, newest

        first.  Single-template sendouts return a 0-or-1 row list; bundle

        sendouts return one row per template the patient completed.


        Scoping

        ~~~~~~~

        The parent sendout is resolved against the practice queryset before

        we list, so a foreign practice's submissions are unreachable even

        when the staff user guesses the bundle's child IDs.  Returns the

        same paginated envelope as every other list endpoint.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: sendout_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedFormSubmissionList'
          description: ''
  /api/v1/practice/{practice_uid}/forms/sendouts/{sendout_id}/submissions/latest/:
    get:
      operationId: practice_forms_sendouts_submissions_latest_retrieve
      description: >-
        ``GET /api/v1/practice/<uid>/forms/sendouts/<id>/submissions/latest/``
        (api#656).


        Convenience endpoint that returns the single most-recent

        :class:`FormSubmission` for ``sendout_id`` -- the row a staff

        member is most likely to want when clicking "View submission"

        on the detail page.  Returns 404 when the sendout has no

        submissions yet (patient hasn't filled the form), so the web

        client can treat the absence as a non-error "nothing to show"

        state without parsing a list envelope.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: sendout_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/practice/{practice_uid}/forms/submissions/{submission_id}/pdf/:
    get:
      operationId: practice_forms_submissions_pdf_retrieve
      description: |-
        GET /api/v1/practice/<uid>/forms/submissions/<id>/pdf/

        Render a completed :class:`FormSubmission` as a human-readable PDF and
        stream it to the caller with ``Content-Type: application/pdf``.

        The same renderer powers the Celery task that auto-attaches the PDF to
        the patient's chart on submit (api#620 / escape#156); this endpoint
        exists so staff can re-download the snapshot on demand without having
        to dig through the doccenter.

        Scoping
        -------
        Practice-scoped — the submission must belong to a sendout owned by the
        URL-supplied practice.  Cross-tenant access produces a 404 rather than
        a 403 so the existence of a foreign submission isn't leaked.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: submission_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/practice/{practice_uid}/forms/templates/:
    get:
      operationId: practice_forms_templates_list
      description: |-
        List or create form templates for a practice.

        ``GET``  /v1/practice/<uid>/forms/templates/?active=true|false|all
        ``POST`` /v1/practice/<uid>/forms/templates/
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedFormTemplateList'
          description: ''
    post:
      operationId: practice_forms_templates_create
      description: |-
        List or create form templates for a practice.

        ``GET``  /v1/practice/<uid>/forms/templates/?active=true|false|all
        ``POST`` /v1/practice/<uid>/forms/templates/
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FormTemplateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/FormTemplateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/FormTemplateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormTemplate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/forms/templates/{template_id}/:
    get:
      operationId: practice_forms_templates_retrieve
      description: |-
        Retrieve or update a form template.

        ``GET``   /v1/practice/<uid>/forms/templates/<id>/
        ``PATCH`` /v1/practice/<uid>/forms/templates/<id>/

        PATCH semantics:
            * Editing a template that has zero submissions updates in place.
            * Editing a template that has any submissions archives the row
              (``is_active=False``) and returns a fresh ``version=N+1`` row with
              the edits applied.  Past submissions stay pinned to the archived
              row.

        PUT is intentionally unsupported — full-replace edits over a versioned
        document are confusing semantically.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormTemplate'
          description: ''
    patch:
      operationId: practice_forms_templates_partial_update
      description: |-
        Retrieve or update a form template.

        ``GET``   /v1/practice/<uid>/forms/templates/<id>/
        ``PATCH`` /v1/practice/<uid>/forms/templates/<id>/

        PATCH semantics:
            * Editing a template that has zero submissions updates in place.
            * Editing a template that has any submissions archives the row
              (``is_active=False``) and returns a fresh ``version=N+1`` row with
              the edits applied.  Past submissions stay pinned to the archived
              row.

        PUT is intentionally unsupported — full-replace edits over a versioned
        document are confusing semantically.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedFormTemplateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedFormTemplateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedFormTemplateRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormTemplate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/forms/templates/{template_id}/archive/:
    post:
      operationId: practice_forms_templates_archive_create
      description: |-
        ``POST /api/v1/practice/<uid>/forms/templates/<id>/archive/``

        Flip ``is_active=False`` on the template so it disappears from the
        active-templates list staff hand out.  Historical sendouts and
        submissions remain pinned to the archived row — versioning is
        immutable — so existing chart entries stay defensible.

        Idempotent: re-archiving an already-archived template is a 200 with
        the current serialized row.  Both archive and re-archive emit the
        ``form_template.archived`` audit event so the trail captures the
        intent regardless of state.

        Conflict
        ~~~~~~~~
        Returns ``409 Conflict`` with ``open_sendout_ids`` when at least one
        :class:`FormSendOut` referencing this template is still in a non-
        resolved status (``pending`` or ``in_progress``).  The web client
        surfaces this as "close N outstanding sendouts first" so staff don't
        accidentally orphan an in-flight patient packet.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/forms/templates/{template_id}/unarchive/:
    post:
      operationId: practice_forms_templates_unarchive_create
      description: |-
        ``POST /api/v1/practice/<uid>/forms/templates/<id>/unarchive/``

        Flip ``is_active=True`` on the template so it reappears in the
        active-templates list.  Idempotent: re-unarchiving an already-active
        template is a 200 with the current serialized row.

        Note that the versioning invariant says only one row per
        ``version_group`` can be active at a time, but the model does NOT
        currently enforce that with a partial unique index.  Unarchive does
        not auto-archive a competing active version — staff are expected to
        use the archive endpoint first if they want to swap which version
        is live, and the API surfaces both endpoints as independent verbs.
        A future hardening pass can add a deferred-unique constraint if the
        competing-versions case becomes a real footgun.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/imaging/capture-token/:
    post:
      operationId: practice_imaging_capture_token_create
      description: >-
        Create a capture session and return a one-time token.


        Called by the web app when staff clicks "Capture X-Rays".

        The token is passed to the desktop app via
        ``escape-xrays://capture?token=...``.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Imaging
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/imaging/mount/:
    get:
      operationId: practice_imaging_mount_list
      description: List mounts in a practice and create new mounts from a template.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedImageMountList'
          description: ''
    post:
      operationId: practice_imaging_mount_create
      description: List mounts in a practice and create new mounts from a template.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImageMountRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ImageMountRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ImageMountRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImageMount'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/imaging/mount-template/:
    get:
      operationId: practice_imaging_mount_template_list
      description: |-
        List canonical mount templates (FMX-18, BW-4, Pano, etc.).

        Templates are not practice-scoped (they're industry standards) but
        the endpoint is mounted under the practice URL so access control
        still runs through ``PracticeScopedMixin``.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedMountTemplateList'
          description: ''
  /api/v1/practice/{practice_uid}/imaging/mount/{mount_uid}/:
    get:
      operationId: practice_imaging_mount_retrieve
      description: Retrieve, update metadata on, or delete a mount.
      parameters:
        - in: path
          name: mount_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImageMount'
          description: ''
    put:
      operationId: practice_imaging_mount_update
      description: Retrieve, update metadata on, or delete a mount.
      parameters:
        - in: path
          name: mount_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImageMountRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ImageMountRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ImageMountRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImageMount'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_imaging_mount_partial_update
      description: Retrieve, update metadata on, or delete a mount.
      parameters:
        - in: path
          name: mount_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedImageMountRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedImageMountRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedImageMountRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImageMount'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_imaging_mount_destroy
      description: Retrieve, update metadata on, or delete a mount.
      parameters:
        - in: path
          name: mount_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/imaging/mount/{mount_uid}/slot/{slot_index}/:
    put:
      operationId: practice_imaging_mount_slot_update
      description: |-
        Update a single slot on a mount (assign/unassign image, rotate, flip).

        Looked up by ``(mount_uid, slot_index)``.  PATCH is the primary verb;
        PUT is supported for completeness but requires all updatable fields.
      parameters:
        - in: path
          name: mount_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: slot_index
          schema:
            type: integer
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImageMountSlotRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ImageMountSlotRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ImageMountSlotRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImageMountSlot'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_imaging_mount_slot_partial_update
      description: |-
        Update a single slot on a mount (assign/unassign image, rotate, flip).

        Looked up by ``(mount_uid, slot_index)``.  PATCH is the primary verb;
        PUT is supported for completeness but requires all updatable fields.
      parameters:
        - in: path
          name: mount_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: slot_index
          schema:
            type: integer
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedImageMountSlotRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedImageMountSlotRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedImageMountSlotRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImageMountSlot'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/imaging/source/:
    get:
      operationId: practice_imaging_source_list
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedSourceList'
          description: ''
    post:
      operationId: practice_imaging_source_create
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SourceRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/SourceRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/SourceRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Source'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/imaging/source/{source_uid}/:
    get:
      operationId: practice_imaging_source_retrieve
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: source_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Source'
          description: ''
    put:
      operationId: practice_imaging_source_update
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: source_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SourceRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/SourceRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/SourceRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Source'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_imaging_source_partial_update
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: source_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedSourceRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedSourceRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedSourceRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Source'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_imaging_source_destroy
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: source_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/imaging/template/:
    get:
      operationId: practice_imaging_template_list
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedExamTemplateList'
          description: ''
    post:
      operationId: practice_imaging_template_create
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExamTemplateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ExamTemplateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ExamTemplateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExamTemplate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/imaging/template/{template_id}/slot/:
    get:
      operationId: practice_imaging_template_slot_list
      description: Manage slots within an exam template.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedExamTemplateSlotList'
          description: ''
    post:
      operationId: practice_imaging_template_slot_create
      description: Manage slots within an exam template.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExamTemplateSlotRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ExamTemplateSlotRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ExamTemplateSlotRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExamTemplateSlot'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/imaging/template/{template_id}/slot/{slot_uid}/:
    delete:
      operationId: practice_imaging_template_slot_destroy
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: slot_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/imaging/template/{template_uid}/:
    get:
      operationId: practice_imaging_template_retrieve
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExamTemplate'
          description: ''
    put:
      operationId: practice_imaging_template_update
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExamTemplateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ExamTemplateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ExamTemplateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExamTemplate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_imaging_template_partial_update
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedExamTemplateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedExamTemplateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedExamTemplateRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExamTemplate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_imaging_template_destroy
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/insurance/carrier/:
    get:
      operationId: practice_insurance_carrier_list
      description: |-
        List and create insurance carriers for a practice.

        Uses :class:`ReferenceDataPagination` (100/2000) instead of the default
        20/100 cap because the web UI loads the full carrier catalog once for
        local typeahead on plan / fee-schedule / claim forms.  A group practice
        with years of imported history can easily carry hundreds of (mostly
        inactive) carriers.  See api#194.
      parameters:
        - in: query
          name: active
          schema:
            type: string
            enum:
              - 'true'
          description: >-
            Filter by active status.  Only ``true`` is honored -- when supplied,
            inactive carriers are hidden.  Omit to return every carrier
            regardless of status.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: search
          schema:
            type: string
          description: Case-insensitive substring search on ``name``.
      tags:
        - Insurance
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedInsuranceCarrierList'
          description: ''
    post:
      operationId: practice_insurance_carrier_create
      description: |-
        List and create insurance carriers for a practice.

        Uses :class:`ReferenceDataPagination` (100/2000) instead of the default
        20/100 cap because the web UI loads the full carrier catalog once for
        local typeahead on plan / fee-schedule / claim forms.  A group practice
        with years of imported history can easily carry hundreds of (mostly
        inactive) carriers.  See api#194.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InsuranceCarrierRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/InsuranceCarrierRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/InsuranceCarrierRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceCarrier'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/insurance/carrier/{carrier_uid}/:
    get:
      operationId: practice_insurance_carrier_retrieve
      description: Retrieve, update, or deactivate an insurance carrier.
      parameters:
        - in: path
          name: carrier_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceCarrier'
          description: ''
    put:
      operationId: practice_insurance_carrier_update
      description: Retrieve, update, or deactivate an insurance carrier.
      parameters:
        - in: path
          name: carrier_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InsuranceCarrierRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/InsuranceCarrierRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/InsuranceCarrierRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceCarrier'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_insurance_carrier_partial_update
      description: Retrieve, update, or deactivate an insurance carrier.
      parameters:
        - in: path
          name: carrier_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedInsuranceCarrierRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedInsuranceCarrierRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedInsuranceCarrierRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceCarrier'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_insurance_carrier_destroy
      description: Retrieve, update, or deactivate an insurance carrier.
      parameters:
        - in: path
          name: carrier_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/insurance/claim/:
    get:
      operationId: practice_insurance_claim_list
      description: >-
        Practice-wide claims list with filtering. For the claims management
        page.
      parameters:
        - in: query
          name: date_from
          schema:
            type: string
            format: date
          description: >-
            Inclusive lower bound on ``date_service`` (ISO 8601 date, e.g.
            ``2025-01-15``).
        - in: query
          name: date_to
          schema:
            type: string
            format: date
          description: Inclusive upper bound on ``date_service`` (ISO 8601 date).
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: string
            format: uuid
          description: Filter claims by the rendering provider UUID.
        - in: query
          name: search
          schema:
            type: string
          description: >-
            Case-insensitive substring search across the patient's
            ``first_name`` and ``last_name``.
        - in: query
          name: status
          schema:
            type: string
            enum:
              - accepted
              - appealed
              - denied
              - draft
              - imported
              - paid
              - partial
              - rejected
              - resubmitted
              - submitted
              - void
          description: Filter claims by their current state-machine status.
      tags:
        - Insurance
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedInsuranceClaimList'
          description: ''
  /api/v1/practice/{practice_uid}/insurance/claims/summary/:
    get:
      operationId: practice_insurance_claims_summary_retrieve
      description: |-
        Aggregate claims counts and dollar amounts grouped by status.

        Returns per-status breakdowns (count, total_fee, total_paid) and
        practice-wide totals including outstanding balance.

        GET /practice/<uid>/insurance/claims/summary/
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/practice/{practice_uid}/insurance/fee-schedule/:
    get:
      operationId: practice_insurance_fee_schedule_list
      description: |-
        List and create fee schedules for a practice.

        Uses :class:`ReferenceDataPagination` (100/2000) instead of the default
        20/100 cap because the web UI loads the full fee-schedule catalog once
        for local dropdown rendering on insurance / plan admin screens.  See
        api#194.
      parameters:
        - in: query
          name: carrier
          schema:
            type: string
            format: uuid
          description: Filter fee schedules by the associated insurance carrier UUID.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: search
          schema:
            type: string
          description: >-
            Case-insensitive substring search across ``name``,
            ``carrier__name``, and ``notes``.
      tags:
        - Insurance
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedFeeScheduleListList'
          description: ''
    post:
      operationId: practice_insurance_fee_schedule_create
      description: |-
        List and create fee schedules for a practice.

        Uses :class:`ReferenceDataPagination` (100/2000) instead of the default
        20/100 cap because the web UI loads the full fee-schedule catalog once
        for local dropdown rendering on insurance / plan admin screens.  See
        api#194.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FeeScheduleRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/FeeScheduleRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/FeeScheduleRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeeSchedule'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/insurance/fee-schedule/{fee_schedule_id}/entry/:
    get:
      operationId: practice_insurance_fee_schedule_entry_list
      description: List entries for a fee schedule.
      parameters:
        - in: path
          name: fee_schedule_id
          schema:
            type: string
            format: uuid
          required: true
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedFeeScheduleEntryList'
          description: ''
  /api/v1/practice/{practice_uid}/insurance/fee-schedule/{fee_schedule_id}/entry/{entry_uid}/:
    delete:
      operationId: practice_insurance_fee_schedule_entry_destroy
      description: Delete a single fee schedule entry.
      parameters:
        - in: path
          name: entry_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: fee_schedule_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/insurance/fee-schedule/{fee_schedule_id}/entry/bulk/:
    post:
      operationId: practice_insurance_fee_schedule_entry_bulk_create
      description: >-
        Bulk create/update fee schedule entries. Accepts a list of {proc_code,
        allowed_amount, description?}.
      parameters:
        - in: path
          name: fee_schedule_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FeeScheduleEntryRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/FeeScheduleEntryRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/FeeScheduleEntryRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeeScheduleEntry'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/insurance/fee-schedule/{fee_schedule_uid}/:
    get:
      operationId: practice_insurance_fee_schedule_retrieve
      description: Retrieve, update, or delete a fee schedule.
      parameters:
        - in: path
          name: fee_schedule_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeeSchedule'
          description: ''
    put:
      operationId: practice_insurance_fee_schedule_update
      description: Retrieve, update, or delete a fee schedule.
      parameters:
        - in: path
          name: fee_schedule_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FeeScheduleRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/FeeScheduleRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/FeeScheduleRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeeSchedule'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_insurance_fee_schedule_partial_update
      description: Retrieve, update, or delete a fee schedule.
      parameters:
        - in: path
          name: fee_schedule_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedFeeScheduleRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedFeeScheduleRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedFeeScheduleRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeeSchedule'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_insurance_fee_schedule_destroy
      description: Retrieve, update, or delete a fee schedule.
      parameters:
        - in: path
          name: fee_schedule_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/insurance/patient-insurance/:
    get:
      operationId: practice_insurance_patient_insurance_list
      description: >-
        Practice-wide patient-insurance list and bulk create.


        GET returns all ``PatientInsurance`` records for the practice
        (paginated,

        filterable by ``patient``, ``carrier``, ``ordinal``).

        POST accepts a JSON array for bulk import (up to 5000 records).  Each

        record must include ``patient`` since the endpoint is not
        patient-scoped.

        PATCH accepts a JSON array of ``{id, ...}`` objects for bulk update.


        Carrier resolution: if a POSTed record includes ``carrier_name`` but no

        ``carrier`` FK, the serializer resolves-or-creates an

        :class:`InsuranceCarrier` row in the current practice (case-insensitive

        match on name) and links the enrollment to it.  This replaces the

        previous behavior where sync tools had to pre-create carriers

        themselves.


        Used by escape-eaglesoft and escape-opendental sync tools to avoid the

        per-patient loop against ``patient/<uid>/insurance/plan/``.
      parameters:
        - in: query
          name: carrier
          schema:
            type: string
            format: uuid
          description: Filter enrollments by the associated insurance carrier UUID.
        - in: query
          name: ordinal
          schema:
            type: integer
          description: >-
            Filter by coverage ordinal.  ``1`` = primary coverage, ``2`` =
            secondary coverage.  Higher values are accepted for practices that
            track tertiary+ coverage.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: query
          name: patient
          schema:
            type: string
            format: uuid
          description: Filter enrollments down to a single patient by patient UUID.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPatientInsuranceList'
          description: ''
    post:
      operationId: practice_insurance_patient_insurance_create
      description: >-
        Practice-wide patient-insurance list and bulk create.


        GET returns all ``PatientInsurance`` records for the practice
        (paginated,

        filterable by ``patient``, ``carrier``, ``ordinal``).

        POST accepts a JSON array for bulk import (up to 5000 records).  Each

        record must include ``patient`` since the endpoint is not
        patient-scoped.

        PATCH accepts a JSON array of ``{id, ...}`` objects for bulk update.


        Carrier resolution: if a POSTed record includes ``carrier_name`` but no

        ``carrier`` FK, the serializer resolves-or-creates an

        :class:`InsuranceCarrier` row in the current practice (case-insensitive

        match on name) and links the enrollment to it.  This replaces the

        previous behavior where sync tools had to pre-create carriers

        themselves.


        Used by escape-eaglesoft and escape-opendental sync tools to avoid the

        per-patient loop against ``patient/<uid>/insurance/plan/``.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientInsuranceBulkWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientInsuranceBulkWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientInsuranceBulkWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientInsuranceBulkWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_insurance_patient_insurance_partial_update
      description: Handle PATCH on the list endpoint for bulk updates.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPatientInsuranceBulkWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPatientInsuranceBulkWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPatientInsuranceBulkWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientInsuranceBulkWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/insurance/plan/:
    get:
      operationId: practice_insurance_plan_list
      description: >-
        List and create insurance plans for a practice. Supports bulk create
        (POST array) and bulk update (PATCH array).


        Uses :class:`ReferenceDataPagination` (100/2000) instead of the default

        20/100 cap because the web UI loads the full plan catalog once for

        local typeahead on patient-insurance, verification, and claim forms.

        Practices routinely accumulate thousands of employer/plan combinations

        over the lifetime of the office.  See api#194.
      parameters:
        - in: query
          name: active
          schema:
            type: string
            enum:
              - 'true'
          description: >-
            Filter by active status.  Only ``true`` is honored -- when supplied,
            inactive plans are hidden.  Omit to return every plan regardless of
            status.
        - in: query
          name: carrier
          schema:
            type: string
            format: uuid
          description: Filter plans by the associated insurance carrier UUID.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: search
          schema:
            type: string
          description: >-
            Case-insensitive substring search across ``plan_name``,
            ``group_name``, ``employer_name``, and ``group_number``.
      tags:
        - Insurance
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedInsurancePlanListList'
          description: ''
    post:
      operationId: practice_insurance_plan_create
      description: >-
        List and create insurance plans for a practice. Supports bulk create
        (POST array) and bulk update (PATCH array).


        Uses :class:`ReferenceDataPagination` (100/2000) instead of the default

        20/100 cap because the web UI loads the full plan catalog once for

        local typeahead on patient-insurance, verification, and claim forms.

        Practices routinely accumulate thousands of employer/plan combinations

        over the lifetime of the office.  See api#194.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InsurancePlanRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/InsurancePlanRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/InsurancePlanRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsurancePlan'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_insurance_plan_bulk_partial_update
      description: Handle PATCH on the list endpoint for bulk updates.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedInsurancePlanRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedInsurancePlanRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedInsurancePlanRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsurancePlan'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/insurance/plan/{plan_id}/benefit/:
    get:
      operationId: practice_insurance_plan_benefit_list
      description: >-
        List benefits attached to a specific insurance plan.  Scope is fixed by
        the ``plan_id`` path parameter; the filter set is intentionally empty --
        narrow the result set by choosing a different plan rather than by query
        string.  Standard ``page`` / ``page_size`` pagination applies.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: plan_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedInsuranceBenefitList'
          description: ''
    post:
      operationId: practice_insurance_plan_benefit_create
      description: |-
        List and create benefits for an insurance plan.

        Uses :class:`ReferenceDataPagination` (100/2000) instead of the default
        20/100 cap because the web UI fetches every benefit for a plan in one
        request to render the full coverage grid (per-category + per-procedure
        overrides).  Imported plans from legacy PMS can carry hundreds of
        benefit rows per plan.  See api#194.
      parameters:
        - in: path
          name: plan_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InsuranceBenefitWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/InsuranceBenefitWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/InsuranceBenefitWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceBenefitWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/insurance/plan/{plan_id}/benefit/{benefit_uid}/:
    get:
      operationId: practice_insurance_plan_benefit_retrieve
      description: Update or delete a plan-level benefit.
      parameters:
        - in: path
          name: benefit_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceBenefit'
          description: ''
    put:
      operationId: practice_insurance_plan_benefit_update
      description: Update or delete a plan-level benefit.
      parameters:
        - in: path
          name: benefit_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InsuranceBenefitWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/InsuranceBenefitWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/InsuranceBenefitWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceBenefitWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_insurance_plan_benefit_partial_update
      description: Update or delete a plan-level benefit.
      parameters:
        - in: path
          name: benefit_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedInsuranceBenefitWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedInsuranceBenefitWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedInsuranceBenefitWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceBenefitWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_insurance_plan_benefit_destroy
      description: Update or delete a plan-level benefit.
      parameters:
        - in: path
          name: benefit_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/insurance/plan/{plan_uid}/:
    get:
      operationId: practice_insurance_plan_retrieve
      description: Retrieve, update, or delete an insurance plan.
      parameters:
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsurancePlan'
          description: ''
    put:
      operationId: practice_insurance_plan_update
      description: Retrieve, update, or delete an insurance plan.
      parameters:
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InsurancePlanRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/InsurancePlanRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/InsurancePlanRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsurancePlan'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_insurance_plan_partial_update
      description: Retrieve, update, or delete an insurance plan.
      parameters:
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedInsurancePlanRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedInsurancePlanRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedInsurancePlanRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsurancePlan'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_insurance_plan_destroy
      description: Retrieve, update, or delete an insurance plan.
      parameters:
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/insurance/verification/:
    get:
      operationId: practice_insurance_verification_list
      description: |-
        List and create insurance verifications for a practice.

        Filters (all optional, combinable):
        * ``patient`` -- UUID
        * ``insurance_plan`` -- UUID
        * ``verified_between`` -- ``from,to`` ISO 8601 datetimes (inclusive)

        Emits ``insurance.verification_viewed`` on GET and
        ``insurance.verification_created`` on POST.
      parameters:
        - in: query
          name: insurance_plan
          schema:
            type: string
            format: uuid
          description: Filter verifications by the associated insurance plan UUID.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: query
          name: patient
          schema:
            type: string
            format: uuid
          description: Filter verifications down to a single patient by patient UUID.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: verified_between
          schema:
            type: string
          description: >-
            Restrict results to verifications whose ``verified_at`` falls inside
            a comma-separated ISO 8601 datetime range, e.g.
            ``2026-04-01T00:00:00Z,2026-04-23T23:59:59Z``.  Both bounds are
            inclusive; omit this filter to retrieve every verification.
      tags:
        - Insurance
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedInsuranceVerificationList'
          description: ''
    post:
      operationId: practice_insurance_verification_create
      description: >-
        Record a new insurance verification.  Verifications are **append-only**:
        this endpoint has no PUT/PATCH/DELETE counterpart.  Re-verifying
        coverage creates a new row so the audit trail of what the payer said,
        and when, is preserved.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InsuranceVerificationRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/InsuranceVerificationRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/InsuranceVerificationRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceVerification'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/insurance/verification/{verification_uid}/:
    get:
      operationId: practice_insurance_verification_retrieve
      description: |-
        Retrieve a single insurance verification.

        Verifications are immutable snapshots -- this view only supports GET.
        Emits ``insurance.verification_viewed`` on each read.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: verification_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceVerification'
          description: ''
  /api/v1/practice/{practice_uid}/intake/arrive/:
    post:
      operationId: practice_intake_arrive_create
      description: |-
        Single round-trip patient lookup + appointment arrival for kiosks.

        ``POST /api/v1/practice/<uid>/intake/arrive/``
        Body: ``{"last_name": "Smith", "date_of_birth": "1980-04-22"}``

        Collapses the legacy three-call kiosk sequence
        (``intake/lookup/`` → ``scheduling/appointment/`` →
        ``scheduling/.../transition/``) into a single signed request so that
        a kiosk on a slow network can mark a patient arrived in one
        round-trip and so the kiosk OAuth token does not need
        ``IsStaffUser`` — only the ``write:patients`` scope.

        Privacy contract
        ----------------
        The 404 response is deliberately generic: the same status fires for
        "no patient match", "no eligible appointment today", and "multiple
        ambiguous patient matches".  This prevents a public kiosk from
        being used as an enumeration oracle (whether a given last
        name + DOB exists in the practice, or whether that patient has an
        appointment).

        Today is computed in the **practice's local timezone**, not the
        server's UTC clock — a remote staff member or a worker process in a
        different region must still see "today" from the office's
        perspective.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Patients
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KioskArriveRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/KioskArriveRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/KioskArriveRequestRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KioskArriveResponse'
          description: ''
        '400':
          description: Missing or malformed request body.
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: >-
            No matching patient, no eligible appointment today, or multiple
            ambiguous matches.  Generic by design — a public kiosk must not be
            usable to enumerate the patient roster.
        '409':
          description: >-
            The matched appointment is not in a state that can transition to
            ``arrived`` (e.g. already arrived, complete, broken).
  /api/v1/practice/{practice_uid}/intake/lookup/:
    get:
      operationId: practice_intake_lookup_retrieve
      description: |-
        Search for an existing patient by last name and date of birth.

        Both ``last_name`` and ``date_of_birth`` are **required** query
        parameters to prevent patient list enumeration from a kiosk.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/practice/{practice_uid}/intake/new-patient-draft/:
    post:
      operationId: practice_intake_new_patient_draft_create
      description: |-
        POST a fresh draft from the kiosk.

        Mirrors :class:`apps.tablets.views_checkin.KioskCheckInView`'s auth
        contract: the calling tablet's OAuth bearer is the auth, and the
        tablet's bound practice must match the URL practice.  A staff
        session is rejected with 401 -- the kiosk submit surface is
        exclusively for in-office tablets so we don't accidentally accept
        a draft from somewhere that isn't running the kiosk wizard.

        On success the draft is created in ``pending`` and the response
        carries only the new draft uid -- the kiosk does not need the
        full record back.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Intake
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewPatientDraftSubmitRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/NewPatientDraftSubmitRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/NewPatientDraftSubmitRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          description: Draft stored; returns the draft uid.
        '400':
          description: Malformed payload.
        '401':
          description: Request is not authenticated as a paired tablet.
        '403':
          description: >-
            Tablet is paired with a different practice.


            Additionally, this endpoint returns 403 with
            ``code=inactive_practice`` or ``code=inactive_client`` when the
            practice or its parent client has been disabled.  See
            ``Inactive403Response`` for the body shape.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/intake/new-patient-drafts/:
    get:
      operationId: practice_intake_new_patient_drafts_list
      description: |-
        List drafts at the calling practice.

        Default filter is ``status=pending`` (the inbox view).  Pass
        ``?status=approved|merged|rejected|all`` to widen the scope.  The
        list endpoint is staff manager+ because the JSON buckets contain
        raw PHI.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Intake
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedNewPatientDraftListItemList'
          description: ''
  /api/v1/practice/{practice_uid}/intake/new-patient-drafts/{draft_uid}/:
    get:
      operationId: practice_intake_new_patient_drafts_retrieve
      description: |-
        Full draft including JSON buckets.

        Logs ``intake.draft_viewed`` on every GET because the body is PHI.
      parameters:
        - in: path
          name: draft_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Intake
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NewPatientDraftDetail'
          description: ''
        '404':
          description: Draft not found at this practice.
  /api/v1/practice/{practice_uid}/intake/new-patient-drafts/{draft_uid}/approve/:
    post:
      operationId: practice_intake_new_patient_drafts_approve_create
      description: Approve a draft, minting a new :class:`apps.patients.models.Patient`.
      parameters:
        - in: path
          name: draft_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Intake
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          description: Patient created; returns the new patient uid.
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Draft not found at this practice.
        '409':
          description: Draft is already in a terminal state.
  /api/v1/practice/{practice_uid}/intake/new-patient-drafts/{draft_uid}/merge/:
    post:
      operationId: practice_intake_new_patient_drafts_merge_create
      description: Merge a draft into an existing :class:`apps.patients.models.Patient`.
      parameters:
        - in: path
          name: draft_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Intake
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewPatientDraftMergeRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/NewPatientDraftMergeRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/NewPatientDraftMergeRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: Draft merged into the target patient.
        '400':
          description: Malformed payload.
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Draft or target patient not found at this practice.
        '409':
          description: Draft is already in a terminal state.
  /api/v1/practice/{practice_uid}/intake/new-patient-drafts/{draft_uid}/reject/:
    post:
      operationId: practice_intake_new_patient_drafts_reject_create
      description: Reject a draft with a free-text reason.
      parameters:
        - in: path
          name: draft_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Intake
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewPatientDraftRejectRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/NewPatientDraftRejectRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/NewPatientDraftRejectRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: Draft rejected; reason stored on the row.
        '400':
          description: Reason missing or empty.
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Draft not found at this practice.
        '409':
          description: Draft is already in a terminal state.
  /api/v1/practice/{practice_uid}/intake/patient/:
    post:
      operationId: practice_intake_patient_create
      description: |-
        Create a new patient from kiosk intake data.

        Creates a Patient with ``status="prospective"`` plus related medical
        history and insurance records in a single atomic transaction.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/intake/patient/{patient_uid}/confirm/:
    put:
      operationId: practice_intake_patient_confirm_update
      description: |-
        Existing patient confirms or updates their data at the kiosk.

        Only provided fields are updated. Sets ``last_medical_review`` to today.
      parameters:
        - in: path
          name: patient_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/kiosk/check-in/:
    post:
      operationId: practice_kiosk_check_in_create
      description: |-
        POST /api/v1/practice/<practice_uid>/kiosk/check-in/.

        Tablet-authenticated.  The DRF default auth chain includes
        :class:`apps.tablets.authentication.TabletOAuth2Authentication`,
        which attaches ``request.tablet`` when the bearer is a paired
        tablet's access token.  Anything else (staff session, third-party
        OAuth app, no token at all) is rejected with 401 -- the kiosk
        check-in surface is exclusively for the in-office tablet, not a
        general write surface.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Tablets
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KioskCheckInRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/KioskCheckInRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/KioskCheckInRequestRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KioskCheckInResponse'
          description: ''
        '400':
          description: Malformed request body.
        '401':
          description: Request is not authenticated as a paired tablet.
        '403':
          description: >-
            Tablet belongs to a different practice.


            Additionally, this endpoint returns 403 with
            ``code=inactive_practice`` or ``code=inactive_client`` when the
            practice or its parent client has been disabled.  See
            ``Inactive403Response`` for the body shape.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: >-
            No patient with that last name + DOB at this practice, OR the
            patient exists but has no eligible appointment today (practice
            timezone).  The two cases collapse to the same status; the kiosk
            shows the same 'please see the front desk' copy for both.
        '409':
          description: >-
            More than one patient at this practice matches the supplied last
            name + DOB (typical: twins).  Cannot safely auto-check-in; the kiosk
            must send the patient to the front desk.
  /api/v1/practice/{practice_uid}/logo/:
    post:
      operationId: practice_logo_create
      description: |-
        Upload or delete the logo for a practice.

        POST /api/v1/practice/{uid}/logo/  -- upload a new logo image
        DELETE /api/v1/practice/{uid}/logo/ -- remove the current logo
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Practices
      requestBody:
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/LogoUploadRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LogoUrlResponse'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_logo_destroy
      description: |-
        Upload or delete the logo for a practice.

        POST /api/v1/practice/{uid}/logo/  -- upload a new logo image
        DELETE /api/v1/practice/{uid}/logo/ -- remove the current logo
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Practices
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/logo/client/:
    post:
      operationId: practice_logo_client_create
      description: >-
        Upload or delete the logo for the client that owns a practice.


        POST /api/v1/practice/{uid}/logo/client/  -- upload a new client logo

        DELETE /api/v1/practice/{uid}/logo/client/ -- remove the current client
        logo
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Practices
      requestBody:
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/LogoUploadRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientLogoUrlResponse'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_logo_client_destroy
      description: >-
        Upload or delete the logo for the client that owns a practice.


        POST /api/v1/practice/{uid}/logo/client/  -- upload a new client logo

        DELETE /api/v1/practice/{uid}/logo/client/ -- remove the current client
        logo
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Practices
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/messaging-settings/:
    get:
      operationId: practice_messaging_settings_retrieve
      description: |-
        GET / PATCH the named messaging inbox owners for a practice.

        See module docstring for the auth / validation / audit contract.
      summary: Read the per-practice messaging inbox owners.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Practices
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/_MessagingSettings'
          description: ''
        '401':
          description: Authentication required.
        '403':
          description: Not a staff user / no access to this practice.
        '404':
          description: Practice not found.
    patch:
      operationId: practice_messaging_settings_update
      description: |-
        GET / PATCH the named messaging inbox owners for a practice.

        See module docstring for the auth / validation / audit contract.
      summary: Replace the per-practice messaging inbox owners.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Practices
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Patched_MessagingSettingsRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/Patched_MessagingSettingsRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/Patched_MessagingSettingsRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/_MessagingSettings'
          description: ''
        '400':
          description: Validation error (cross-tenant or inactive user).
        '401':
          description: Authentication required.
        '403':
          description: >-
            Not a staff user / not a manager-or-above on this practice.


            Additionally, this endpoint returns 403 with
            ``code=inactive_practice`` or ``code=inactive_client`` when the
            practice or its parent client has been disabled.  See
            ``Inactive403Response`` for the body shape.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Practice not found.
  /api/v1/practice/{practice_uid}/messaging/conversation/:
    get:
      operationId: practice_messaging_conversation_list
      description: |-
        List conversations the user participates in, or create a new one.

        GET returns a paginated list of conversations ordered by most recent
        activity, filtered to only those the requesting user is a participant
        of.  POST creates a new conversation and optionally sends the first
        message.
      summary: List conversations
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Messaging
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ConversationList'
          description: ''
    post:
      operationId: practice_messaging_conversation_create
      description: |-
        List conversations the user participates in, or create a new one.

        GET returns a paginated list of conversations ordered by most recent
        activity, filtered to only those the requesting user is a participant
        of.  POST creates a new conversation and optionally sends the first
        message.
      summary: Create conversation
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Messaging
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateConversationRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/CreateConversationRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/CreateConversationRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationDetail'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/messaging/conversation/{conversation_id}/:
    get:
      operationId: practice_messaging_conversation_retrieve
      description: |-
        Retrieve a single conversation.

        Direct conversations require explicit participant membership.
        Operatory conversations are visible to any active practice member.
      summary: Retrieve conversation
      parameters:
        - in: path
          name: conversation_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Messaging
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationDetail'
          description: ''
  /api/v1/practice/{practice_uid}/messaging/conversation/{conversation_id}/messages/:
    get:
      operationId: practice_messaging_conversation_messages_retrieve
      description: |-
        List messages in a conversation or send a new one.

        GET returns messages in chronological order (oldest first), paginated.
        POST creates a new message and updates the conversation's modified
        timestamp so it bubbles to the top of the list.

        Operatory conversations are accessible to every practice member.
      summary: List messages
      parameters:
        - in: path
          name: conversation_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Messaging
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
    post:
      operationId: practice_messaging_conversation_messages_create
      description: |-
        List messages in a conversation or send a new one.

        GET returns messages in chronological order (oldest first), paginated.
        POST creates a new message and updates the conversation's modified
        timestamp so it bubbles to the top of the list.

        Operatory conversations are accessible to every practice member.
      summary: Send message
      parameters:
        - in: path
          name: conversation_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Messaging
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/messaging/conversation/{conversation_id}/read/:
    patch:
      operationId: practice_messaging_conversation_read_partial_update
      description: |-
        Update the requesting user's last_read_at timestamp.

        PATCH (no body required) sets ``last_read_at`` to now, clearing the
        unread badge for this conversation.

        For operatory conversations the user may not have an explicit
        participant record yet, so we create one on-the-fly via
        ``update_or_create``.
      summary: Mark conversation as read
      parameters:
        - in: path
          name: conversation_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Messaging
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/messaging/operatory-presence/:
    get:
      operationId: practice_messaging_operatory_presence_list
      description: |-
        Return every active chair-presence row in this practice.

        Powers the messaging-menu's "OP 3 -- Jane S." occupant labels.  Only
        rows where ``released_at IS NULL`` are returned; auto-released rows
        are filtered out so the UI never shows a stale claim past the
        8-hour sanity cutoff.
      summary: List active chair-presence rows
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Messaging
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/OperatoryPresence'
          description: ''
  /api/v1/practice/{practice_uid}/messaging/operatory-presence/claim/:
    post:
      operationId: practice_messaging_operatory_presence_claim_create
      description: |-
        Claim a chair as the calling user's current operatory.

        POST body: ``{"operatory_id": "<uuid>"}``.

        Idempotent: a repeat claim on the same chair returns ``200`` with
        the existing row.  A claim on a *different* chair releases the
        caller's previous claim first (a user is at one chair at a time).
      summary: Claim an operatory (chair presence)
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Messaging
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OperatoryPresenceWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OperatoryPresenceWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/OperatoryPresenceWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperatoryPresence'
          description: ''
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperatoryPresence'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/messaging/operatory-presence/operatory/{operatory_uid}/:
    get:
      operationId: practice_messaging_operatory_presence_operatory_retrieve
      description: |-
        Return the active occupant for ``<operatory_uid>``, or 404.

        Used when the messaging menu wants a single chair's occupant without
        pulling the practice-wide list (e.g. opening a thread directly).
      summary: Get the active occupant for a specific chair
      parameters:
        - in: path
          name: operatory_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Messaging
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperatoryPresence'
          description: ''
        '404':
          description: No response body
  /api/v1/practice/{practice_uid}/messaging/operatory-presence/release/:
    post:
      operationId: practice_messaging_operatory_presence_release_create
      description: |-
        Release the calling user's claim on an operatory.

        POST body: ``{"operatory_id": "<uuid>"}``.  Returns 200 with
        ``{"released": true|false}`` -- ``false`` indicating the user had no
        active claim on the chair (still a success, just a no-op).
      summary: Release the caller's chair presence
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Messaging
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OperatoryPresenceWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OperatoryPresenceWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/OperatoryPresenceWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: No response body
  /api/v1/practice/{practice_uid}/messaging/operatory/{operatory_id}/:
    get:
      operationId: practice_messaging_operatory_retrieve
      description: |-
        Convenience endpoint for operatory-addressed messaging.

        ``GET  .../operatory/<operatory_id>/`` — returns the operatory's
        conversation, creating it if one doesn't exist yet.

        ``POST .../operatory/<operatory_id>/`` — finds (or creates) the
        operatory conversation and sends a message in a single round-trip.
      summary: Get or create operatory conversation
      parameters:
        - in: path
          name: operatory_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Messaging
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
    post:
      operationId: practice_messaging_operatory_create
      description: |-
        Convenience endpoint for operatory-addressed messaging.

        ``GET  .../operatory/<operatory_id>/`` — returns the operatory's
        conversation, creating it if one doesn't exist yet.

        ``POST .../operatory/<operatory_id>/`` — finds (or creates) the
        operatory conversation and sends a message in a single round-trip.
      summary: Send message to operatory conversation
      parameters:
        - in: path
          name: operatory_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Messaging
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/messaging/presence/:
    get:
      operationId: practice_messaging_presence_retrieve
      description: >-
        Return online/offline status and ``last_seen_at`` for all practice
        members.


        ``GET /api/v1/practice/<uid>/messaging/presence/``


        A member is considered *online* if they have an active WebSocket

        connection.  Since presence is tracked via in-memory channel-layer

        groups (not database state), this endpoint reports ``last_seen_at``

        from the User model as a fallback for offline users.  The frontend

        should use the ``presence.update`` WebSocket event for real-time

        changes and this endpoint only for initial page load.
      summary: List practice member presence
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Messaging
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/practice/{practice_uid}/patient/:
    get:
      operationId: practice_patient_list
      description: |-
        List and create patients. Staff-only — patient users cannot access.

        Supports the reporting filters for the New Patient report (api#153) and
        the Inactive Patient report (api#154).  Both compute their "visit" date
        from the patient's completed appointments (``complete`` /
        ``ready_for_walkout``), bucketed in the practice's timezone, so a 23:30
        appointment in Los Angeles doesn't spill into tomorrow because the
        server happens to speak UTC.

        The first-visit and last-visit annotations are attached only when a
        corresponding filter is active or ``include=first_visit`` /
        ``include=last_visit`` is supplied -- they each add a correlated
        ``Subquery`` and we don't want every typeahead request paying for them.
      parameters:
        - in: query
          name: as_of
          schema:
            type: string
            format: date
          description: >-
            Reference date for the ``is_still_active`` annotation (YYYY-MM-DD,
            practice TZ).  Defaults to today in the practice timezone.  Only
            meaningful when ``include=first_visit`` is set --
            ``is_still_active`` is ``true`` when the patient has at least one
            completed appointment in the 90 days preceding ``as_of``
            (inclusive).
        - in: query
          name: first_visit_date_from
          schema:
            type: string
            format: date
          description: >-
            Include only patients whose **first completed visit** (earliest
            ``complete`` or ``ready_for_walkout`` appointment, bucketed in
            practice TZ) is on or after this date (inclusive). Used by the New
            Patient report.
        - in: query
          name: first_visit_date_to
          schema:
            type: string
            format: date
          description: >-
            Include only patients whose **first completed visit** is on or
            before this date (inclusive). Combine with ``first_visit_date_from``
            to form a closed range.
        - in: query
          name: first_visit_production_gte
          schema:
            type: number
            format: double
          description: >-
            Include only patients whose first-visit production is greater than
            or equal to this value.  Requires the first-visit include group to
            be active (``include=first_visit`` or
            ``first_visit_date_from``/``first_visit_date_to``).  Used by the New
            Patient report to exclude walk-in exams that never converted into
            real production.
        - in: query
          name: has_future_appointment
          schema:
            type: boolean
          description: >-
            When ``false``, exclude patients with any scheduled future
            appointment (``scheduled`` / ``arrived`` / ``seated`` /
            ``ready_for_doctor`` / ``ready_for_walkout`` with ``date_time`` in
            the future).  When ``true``, include only those patients.  Default:
            no filtering on future appointments.
        - in: query
          name: include
          schema:
            type: string
          description: >-
            Comma-separated list of optional serializer field groups to include
            on every row. Supported values: ``first_visit`` (adds
            ``first_visit_date``, ``first_visit_production``,
            ``first_visit_provider_id``, ``first_visit_provider_name``,
            ``is_still_active``), ``last_visit`` (adds ``last_visit_date``). 
            The ``first_visit_*`` fields are also returned automatically when
            ``first_visit_date_from`` / ``first_visit_date_to`` is set;
            ``last_visit_date`` is returned automatically when
            ``last_visit_before`` is set.
        - in: query
          name: last_visit_before
          schema:
            type: string
            format: date
          description: >-
            Include only patients whose **most recent completed visit** is on or
            before this date (inclusive). Used by the Inactive Patient report.
        - in: query
          name: ordering
          schema:
            type: string
          description: >-
            Ordering: ``first_visit_date`` / ``-first_visit_date``,
            ``last_visit_date`` / ``-last_visit_date``.  Nulls are sorted last
            in either direction so patients without any completed visits don't
            crowd the top of the list.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: query
          name: phone
          schema:
            type: string
          description: >-
            Filter to patients whose ``phone_mobile``, ``phone_home``, or
            ``phone_work`` matches the supplied phone number after E.164
            normalisation on **both** sides. Accepts any phone-shaped input
            (``+15551234567``, ``555-123-4567``, ``(555) 123-4567``); stored
            values are also re-normalised at match time, so a query for
            ``+15551234567`` matches a stored ``(555) 123-4567`` or
            ``5551234567``.  Used by the call-pop screen to fetch a pre-filtered
            list instead of paging the full patient table.  Unparseable input
            returns an empty list (200, not 400) — matches the spirit of the
            other forgiving query params on this endpoint.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: array
            items:
              type: string
              format: uuid
          description: >-
            Filter by provider UUID.  May be repeated
            (``?provider=<u1>&provider=<u2>``) or comma-separated
            (``?provider=<u1>,<u2>``) to match any of several providers (union /
            OR).  When the first-visit include group is active
            (``include=first_visit`` or
            ``first_visit_date_from``/``first_visit_date_to`` is set), matches
            against the patient's first-visit provider -- i.e. who they saw on
            their earliest completed appointment.  Otherwise matches against
            ``primary_provider``.
        - in: query
          name: search
          schema:
            type: string
          description: >-
            Case-insensitive substring search.  Single-token queries fan out
            across ``first_name``, ``last_name``, ``email``,
            ``phone_home``/``phone_work``/``phone_mobile``, ``date_of_birth``,
            ``address``, ``city``, ``state``, and ``zip_code``.  Multi-token
            queries (e.g. ``aaron de bruyn``) are treated as name searches:
            every token must match ``first_name`` or ``last_name`` (AND across
            tokens, OR across the two name fields), which prevents accidental
            substring hits inside email/phone/address from returning unrelated
            patients.  Also powers the patient-search typeahead; the
            ``TypeaheadPageNumberPagination`` cap (25/50) keeps autocomplete
            responses tight for UI clients.
        - in: query
          name: status
          schema:
            type: string
            enum:
              - active
              - archived
              - deceased
              - inactive
              - prospective
          description: Filter by patient status.
      tags:
        - Patients
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPatientList'
          description: ''
    post:
      operationId: practice_patient_create
      description: |-
        List and create patients. Staff-only — patient users cannot access.

        Supports the reporting filters for the New Patient report (api#153) and
        the Inactive Patient report (api#154).  Both compute their "visit" date
        from the patient's completed appointments (``complete`` /
        ``ready_for_walkout``), bucketed in the practice's timezone, so a 23:30
        appointment in Los Angeles doesn't spill into tomorrow because the
        server happens to speak UTC.

        The first-visit and last-visit annotations are attached only when a
        corresponding filter is active or ``include=first_visit`` /
        ``include=last_visit`` is supplied -- they each add a correlated
        ``Subquery`` and we don't want every typeahead request paying for them.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Patients
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Patient'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_bulk_partial_update
      description: Handle PATCH on the list endpoint for bulk updates.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Patients
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPatientRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPatientRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPatientRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Patient'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient-inbox/:
    get:
      operationId: practice_patient_inbox_list
      description: |-
        Staff-facing inbox for unreviewed :class:`NewPatientDraft` rows.

        Mounted at ``/api/v1/practice/<uid>/patient-inbox/`` with the
        standard ViewSet routes plus three @action transitions:

        * ``POST .../<id>/convert/``  -- mint a new patient.
        * ``POST .../<id>/merge/``    -- fold into an existing patient.
        * ``POST .../<id>/dismiss/``  -- terminal, no patient created.

        The endpoint is manager-or-above on read AND write because the
        JSON buckets ARE PHI (api#770 mirrors the api#761 pattern); the
        ``enforce_role_on_read = True`` flag is what makes the role gate
        fire on GET as well as POST.

        Audit
        -----
        Every state change emits an ``intake.*`` semantic event with PHI-
        scrubbed metadata.  List + retrieve also emit access events so a
        HIPAA reviewer can reconstruct who read the staging table.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: status
          schema:
            type: string
            enum:
              - all
              - approved
              - converted
              - dismissed
              - merged
              - pending
              - ready_for_review
              - rejected
          description: >-
            Filter drafts by lifecycle status.  Default is the open states
            (``pending`` + ``ready_for_review``); pass an explicit value to
            widen, or ``all`` for every state.
      tags:
        - Patient Inbox
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPatientInboxListItemList'
          description: ''
  /api/v1/practice/{practice_uid}/patient-inbox/{id}/:
    get:
      operationId: practice_patient_inbox_retrieve
      description: |-
        Staff-facing inbox for unreviewed :class:`NewPatientDraft` rows.

        Mounted at ``/api/v1/practice/<uid>/patient-inbox/`` with the
        standard ViewSet routes plus three @action transitions:

        * ``POST .../<id>/convert/``  -- mint a new patient.
        * ``POST .../<id>/merge/``    -- fold into an existing patient.
        * ``POST .../<id>/dismiss/``  -- terminal, no patient created.

        The endpoint is manager-or-above on read AND write because the
        JSON buckets ARE PHI (api#770 mirrors the api#761 pattern); the
        ``enforce_role_on_read = True`` flag is what makes the role gate
        fire on GET as well as POST.

        Audit
        -----
        Every state change emits an ``intake.*`` semantic event with PHI-
        scrubbed metadata.  List + retrieve also emit access events so a
        HIPAA reviewer can reconstruct who read the staging table.
      parameters:
        - in: path
          name: id
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Patient Inbox
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientInboxDetail'
          description: ''
  /api/v1/practice/{practice_uid}/patient-inbox/{id}/convert/:
    post:
      operationId: practice_patient_inbox_convert_create
      description: |-
        Convert a draft into a brand-new :class:`Patient`.

        Mints a fresh patient row from the draft's structured fields,
        links it via ``converted_to_patient``, flips the draft into
        ``converted``, and audit-logs the transition.  Returns the
        same shape the legacy approve endpoint emits plus the source
        draft id for the staff UI's success toast.
      parameters:
        - in: path
          name: id
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Patient Inbox
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          description: Patient created; returns the draft id + new patient id.
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Draft not found at this practice.
        '409':
          description: Draft is already in a terminal state.
  /api/v1/practice/{practice_uid}/patient-inbox/{id}/dismiss/:
    post:
      operationId: practice_patient_inbox_dismiss_create
      description: |-
        Dismiss a draft as spam / accidental submission.

        Reason is optional; staff who supply one get it persisted on
        the row for audit.  No patient is created or modified.
      parameters:
        - in: path
          name: id
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Patient Inbox
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientInboxDismissRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientInboxDismissRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientInboxDismissRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: Draft dismissed.
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Draft not found at this practice.
        '409':
          description: Draft is already in a terminal state.
  /api/v1/practice/{practice_uid}/patient-inbox/{id}/merge/:
    post:
      operationId: practice_patient_inbox_merge_create
      description: |-
        Fold a draft into an existing :class:`Patient`.

        Demographics on the target are left alone -- the merge only
        attaches the draft's insurance + medical-history child rows.
        Staff who want to refresh the patient's name / DOB / address
        should use the regular patient-edit surface.
      parameters:
        - in: path
          name: id
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Patient Inbox
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientInboxMergeRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientInboxMergeRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientInboxMergeRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: Draft merged into the target patient.
        '400':
          description: Malformed payload.
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Draft or target patient not found at this practice.
        '409':
          description: Draft is already in a terminal state.
  /api/v1/practice/{practice_uid}/patient/{patient_id}/billing/adjustment/:
    get:
      operationId: practice_patient_billing_adjustment_list
      description: >-
        Paginated adjustment history for a patient.  Scope is fixed by the
        ``patient_id`` path parameter; the filter set is intentionally empty. 
        Standard ``page`` / ``page_size`` pagination applies.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Billing
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedAdjustmentList'
          description: ''
    post:
      operationId: practice_patient_billing_adjustment_create
      description: >-
        Paginated adjustment history for a patient. POST to create a new
        adjustment.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Billing
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdjustmentWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AdjustmentWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/AdjustmentWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdjustmentWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/billing/balance/:
    get:
      operationId: practice_patient_billing_balance_retrieve
      description: GET patient balance. Returns 404 if no balance record exists.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientBalance'
          description: ''
  /api/v1/practice/{practice_uid}/patient/{patient_id}/billing/balance-summary/:
    get:
      operationId: practice_patient_billing_balance_summary_retrieve
      description: GET computed balance from actual ledger data (not stored snapshot).
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Billing
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceSummary'
          description: ''
  /api/v1/practice/{practice_uid}/patient/{patient_id}/billing/checkout/:
    post:
      operationId: practice_patient_billing_checkout_create
      description: >-
        POST checkout/walkout — complete an appointment with procedures,
        payment, and optional claim.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Billing
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckoutRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/CheckoutRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/CheckoutRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckoutResponse'
          description: ''
        '400':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/billing/payment/:
    get:
      operationId: practice_patient_billing_payment_list
      description: >-
        Paginated payment history for a patient.  Scope is fixed by the
        ``patient_id`` path parameter; the filter set is intentionally empty --
        use the practice-wide endpoint (``/billing/payment/``) when
        cross-patient filtering is required.  Standard ``page`` / ``page_size``
        pagination applies.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Billing
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPaymentRecordList'
          description: ''
    post:
      operationId: practice_patient_billing_payment_create
      description: Paginated payment history for a patient. POST to create a new payment.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Billing
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentRecordWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PaymentRecordWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PaymentRecordWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentRecordWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/billing/payment-plan/:
    get:
      operationId: practice_patient_billing_payment_plan_list
      description: >-
        Payment plans for a patient.  Scope is fixed by the ``patient_id`` path
        parameter; the filter set is intentionally empty.  Standard ``page`` /
        ``page_size`` pagination applies.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Billing
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPaymentPlanList'
          description: ''
    post:
      operationId: practice_patient_billing_payment_plan_create
      description: Payment plans for a patient. Unpaginated. POST to create a new plan.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Billing
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentPlanWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PaymentPlanWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PaymentPlanWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentPlanWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/billing/payment-plan/{plan_uid}/:
    get:
      operationId: practice_patient_billing_payment_plan_retrieve
      description: Retrieve or update a payment plan.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentPlan'
          description: ''
    put:
      operationId: practice_patient_billing_payment_plan_update
      description: Retrieve or update a payment plan.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentPlanWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PaymentPlanWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PaymentPlanWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentPlanWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_billing_payment_plan_partial_update
      description: Retrieve or update a payment plan.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPaymentPlanWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPaymentPlanWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPaymentPlanWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentPlanWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/billing/payment/{payment_uid}/:
    get:
      operationId: practice_patient_billing_payment_retrieve
      description: Retrieve or update a payment record.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: payment_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentRecord'
          description: ''
    put:
      operationId: practice_patient_billing_payment_update
      description: Retrieve or update a payment record.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: payment_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentRecordWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PaymentRecordWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PaymentRecordWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentRecordWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_billing_payment_partial_update
      description: Retrieve or update a payment record.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: payment_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPaymentRecordWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPaymentRecordWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPaymentRecordWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentRecordWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/alerts/:
    get:
      operationId: practice_patient_clinical_alerts_retrieve
      description: |-
        Single-patient medical alert summary.

        GET /practice/<uid>/patient/<patient_id>/clinical/alerts/
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/allergy/:
    get:
      operationId: practice_patient_clinical_allergy_list
      description: List and create allergies for a patient.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPatientAllergyList'
          description: ''
    post:
      operationId: practice_patient_clinical_allergy_create
      description: List and create allergies for a patient.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientAllergyRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientAllergyRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientAllergyRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientAllergy'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/allergy/{allergy_uid}/:
    get:
      operationId: practice_patient_clinical_allergy_retrieve
      description: Retrieve, update, or delete a patient allergy.
      parameters:
        - in: path
          name: allergy_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientAllergy'
          description: ''
    put:
      operationId: practice_patient_clinical_allergy_update
      description: Retrieve, update, or delete a patient allergy.
      parameters:
        - in: path
          name: allergy_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientAllergyRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientAllergyRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientAllergyRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientAllergy'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_clinical_allergy_partial_update
      description: Retrieve, update, or delete a patient allergy.
      parameters:
        - in: path
          name: allergy_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPatientAllergyRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPatientAllergyRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPatientAllergyRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientAllergy'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_patient_clinical_allergy_destroy
      description: Retrieve, update, or delete a patient allergy.
      parameters:
        - in: path
          name: allergy_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/anesthesia/:
    get:
      operationId: practice_patient_clinical_anesthesia_list
      description: |-
        List and create anesthesia records for a patient.

        Supports filtering by appointment and date range.
      parameters:
        - in: query
          name: appointment
          schema:
            type: string
            format: uuid
          description: Filter records to a single appointment UUID.
        - in: query
          name: date_from
          schema:
            type: string
            format: date
          description: >-
            Inclusive lower bound on ``administered_at`` (ISO 8601 date).
            Interpreted in the practice's local timezone.
        - in: query
          name: date_to
          schema:
            type: string
            format: date
          description: >-
            Inclusive upper bound on ``administered_at`` (ISO 8601 date).
            Interpreted in the practice's local timezone.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedAnesthesiaRecordList'
          description: ''
    post:
      operationId: practice_patient_clinical_anesthesia_create
      description: |-
        List and create anesthesia records for a patient.

        Supports filtering by appointment and date range.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnesthesiaRecordWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AnesthesiaRecordWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/AnesthesiaRecordWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnesthesiaRecordWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/anesthesia/{anesthesia_uid}/:
    get:
      operationId: practice_patient_clinical_anesthesia_retrieve
      description: Retrieve or update an anesthesia record.
      parameters:
        - in: path
          name: anesthesia_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnesthesiaRecord'
          description: ''
    put:
      operationId: practice_patient_clinical_anesthesia_update
      description: Retrieve or update an anesthesia record.
      parameters:
        - in: path
          name: anesthesia_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnesthesiaRecordWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AnesthesiaRecordWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/AnesthesiaRecordWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnesthesiaRecordWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_clinical_anesthesia_partial_update
      description: Retrieve or update an anesthesia record.
      parameters:
        - in: path
          name: anesthesia_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedAnesthesiaRecordWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedAnesthesiaRecordWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedAnesthesiaRecordWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnesthesiaRecordWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/condition/:
    get:
      operationId: practice_patient_clinical_condition_list
      description: List and create medical conditions for a patient.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPatientConditionList'
          description: ''
    post:
      operationId: practice_patient_clinical_condition_create
      description: List and create medical conditions for a patient.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientConditionRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientConditionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientConditionRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientCondition'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/condition/{condition_uid}/:
    get:
      operationId: practice_patient_clinical_condition_retrieve
      description: Retrieve, update, or delete a patient medical condition.
      parameters:
        - in: path
          name: condition_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientCondition'
          description: ''
    put:
      operationId: practice_patient_clinical_condition_update
      description: Retrieve, update, or delete a patient medical condition.
      parameters:
        - in: path
          name: condition_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientConditionRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientConditionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientConditionRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientCondition'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_clinical_condition_partial_update
      description: Retrieve, update, or delete a patient medical condition.
      parameters:
        - in: path
          name: condition_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPatientConditionRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPatientConditionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPatientConditionRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientCondition'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_patient_clinical_condition_destroy
      description: Retrieve, update, or delete a patient medical condition.
      parameters:
        - in: path
          name: condition_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/confirm-medical-review/:
    post:
      operationId: practice_patient_clinical_confirm_medical_review_create
      description: |-
        Confirm that a patient's medical history has been reviewed.

        POST sets ``patient.last_medical_review`` to today and
        ``patient.last_medical_review_by`` to the authenticated user.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/education/:
    get:
      operationId: practice_patient_clinical_education_list
      description: |-
        List and create patient education records for a patient.

        Supports filtering by appointment, topic, and date range.
      parameters:
        - in: query
          name: appointment
          schema:
            type: string
            format: uuid
          description: Filter records to a single appointment UUID.
        - in: query
          name: date_from
          schema:
            type: string
            format: date
          description: Inclusive lower bound on ``education_date`` (ISO 8601 date).
        - in: query
          name: date_to
          schema:
            type: string
            format: date
          description: Inclusive upper bound on ``education_date`` (ISO 8601 date).
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: topic
          schema:
            type: string
            enum:
              - brushing
              - bruxism
              - caries_risk
              - diet_counseling
              - dry_mouth
              - electric_toothbrush
              - flossing
              - fluoride
              - implant_care
              - interproximal
              - oral_hygiene
              - ortho_care
              - other
              - perio_disease
              - post_op
              - pre_med
              - sensitivity
              - tobacco_cessation
              - whitening
          description: Filter by education topic.
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPatientEducationList'
          description: ''
    post:
      operationId: practice_patient_clinical_education_create
      description: |-
        List and create patient education records for a patient.

        Supports filtering by appointment, topic, and date range.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientEducationWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientEducationWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientEducationWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientEducationWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/education/{education_uid}/:
    get:
      operationId: practice_patient_clinical_education_retrieve
      description: Retrieve, update, or delete a patient education record.
      parameters:
        - in: path
          name: education_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientEducation'
          description: ''
    put:
      operationId: practice_patient_clinical_education_update
      description: Retrieve, update, or delete a patient education record.
      parameters:
        - in: path
          name: education_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientEducationWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientEducationWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientEducationWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientEducationWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_clinical_education_partial_update
      description: Retrieve, update, or delete a patient education record.
      parameters:
        - in: path
          name: education_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPatientEducationWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPatientEducationWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPatientEducationWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientEducationWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_patient_clinical_education_destroy
      description: Retrieve, update, or delete a patient education record.
      parameters:
        - in: path
          name: education_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/medication/:
    get:
      operationId: practice_patient_clinical_medication_list
      description: List and create medications for a patient.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPatientMedicationList'
          description: ''
    post:
      operationId: practice_patient_clinical_medication_create
      description: List and create medications for a patient.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientMedicationRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientMedicationRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientMedicationRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientMedication'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/medication/{medication_uid}/:
    get:
      operationId: practice_patient_clinical_medication_retrieve
      description: Retrieve, update, or delete a patient medication.
      parameters:
        - in: path
          name: medication_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientMedication'
          description: ''
    put:
      operationId: practice_patient_clinical_medication_update
      description: Retrieve, update, or delete a patient medication.
      parameters:
        - in: path
          name: medication_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientMedicationRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientMedicationRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientMedicationRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientMedication'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_clinical_medication_partial_update
      description: Retrieve, update, or delete a patient medication.
      parameters:
        - in: path
          name: medication_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPatientMedicationRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPatientMedicationRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPatientMedicationRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientMedication'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_patient_clinical_medication_destroy
      description: Retrieve, update, or delete a patient medication.
      parameters:
        - in: path
          name: medication_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/note/:
    get:
      operationId: practice_patient_clinical_note_list
      description: >-
        List and create clinical notes for a patient.


        Supports filtering by note_type, provider, date range, and free-text
        search.
      parameters:
        - in: query
          name: date_from
          schema:
            type: string
            format: date
          description: >-
            Inclusive lower bound on ``note_date`` (ISO 8601 date, e.g.
            ``2025-01-15``).
        - in: query
          name: date_to
          schema:
            type: string
            format: date
          description: Inclusive upper bound on ``note_date`` (ISO 8601 date).
        - in: query
          name: note_type
          schema:
            type: string
            enum:
              - clinical
              - consult
              - exam
              - hygiene
              - lab
              - other
              - phone
          description: Filter by note type.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: string
            format: uuid
          description: Filter by provider UUID.
        - in: query
          name: search
          schema:
            type: string
          description: >-
            Case-insensitive substring search across ``subject`` and
            ``content``.
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedClinicalNoteList'
          description: ''
    post:
      operationId: practice_patient_clinical_note_create
      description: >-
        List and create clinical notes for a patient.


        Supports filtering by note_type, provider, date range, and free-text
        search.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClinicalNoteWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ClinicalNoteWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ClinicalNoteWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClinicalNoteWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/note/{note_uid}/:
    get:
      operationId: practice_patient_clinical_note_retrieve
      description: >-
        Retrieve or update a clinical note. PATCH is blocked if the note is
        signed.
      parameters:
        - in: path
          name: note_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClinicalNote'
          description: ''
    put:
      operationId: practice_patient_clinical_note_update
      description: >-
        Retrieve or update a clinical note. PATCH is blocked if the note is
        signed.
      parameters:
        - in: path
          name: note_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClinicalNoteWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ClinicalNoteWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ClinicalNoteWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClinicalNoteWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_clinical_note_partial_update
      description: >-
        Retrieve or update a clinical note. PATCH is blocked if the note is
        signed.
      parameters:
        - in: path
          name: note_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedClinicalNoteWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedClinicalNoteWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedClinicalNoteWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClinicalNoteWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/note/{note_uid}/sign/:
    post:
      operationId: practice_patient_clinical_note_sign_create
      description: |-
        Sign a clinical note (irreversible HIPAA action).

        POST signs the note, setting is_signed=True, signed_by, and signed_at.
        Once signed, the note content becomes immutable.
      parameters:
        - in: path
          name: note_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/perio/:
    get:
      operationId: practice_patient_clinical_perio_list
      description: >-
        List and create periodontal exams for a patient.


        GET returns exams ordered by most recent first, with readings
        prefetched.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPerioExamList'
          description: ''
    post:
      operationId: practice_patient_clinical_perio_create
      description: >-
        List and create periodontal exams for a patient.


        GET returns exams ordered by most recent first, with readings
        prefetched.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PerioExamWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PerioExamWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PerioExamWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PerioExamWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/perio/{perio_uid}/:
    get:
      operationId: practice_patient_clinical_perio_retrieve
      description: |-
        Retrieve, update, or delete a periodontal exam.

        Completed exams cannot be deleted (returns 422).
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: perio_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PerioExam'
          description: ''
    put:
      operationId: practice_patient_clinical_perio_update
      description: |-
        Retrieve, update, or delete a periodontal exam.

        Completed exams cannot be deleted (returns 422).
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: perio_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PerioExamWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PerioExamWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PerioExamWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PerioExamWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_clinical_perio_partial_update
      description: |-
        Retrieve, update, or delete a periodontal exam.

        Completed exams cannot be deleted (returns 422).
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: perio_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPerioExamWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPerioExamWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPerioExamWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PerioExamWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_patient_clinical_perio_destroy
      description: |-
        Retrieve, update, or delete a periodontal exam.

        Completed exams cannot be deleted (returns 422).
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: perio_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/perio/{perio_uid}/readings/:
    put:
      operationId: practice_patient_clinical_perio_readings_update
      description: |-
        Bulk upsert perio readings for an exam.

        PUT accepts an array of reading objects keyed by tooth_num.
        Uses update_or_create per tooth within the exam.
        Returns the full exam with nested readings after save.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: perio_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/perio/compare/:
    get:
      operationId: practice_patient_clinical_perio_compare_retrieve
      description: |-
        Compare two perio exams side by side.

        GET /...perio/compare/?exam1=<id>&exam2=<id>
        Validates both exams belong to the same practice and patient.
      parameters:
        - in: query
          name: exam1
          schema:
            type: string
            format: uuid
          description: First perio exam UUID to compare.
          required: true
        - in: query
          name: exam2
          schema:
            type: string
            format: uuid
          description: Second perio exam UUID to compare.
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/procedure/:
    get:
      operationId: practice_patient_clinical_procedure_list
      description: >-
        List and create procedures for a patient.


        Supports filtering by provider, date range, status, and free-text
        search.
      parameters:
        - in: query
          name: date_from
          schema:
            type: string
            format: date
          description: Inclusive lower bound on ``proc_date`` (ISO 8601 date).
        - in: query
          name: date_to
          schema:
            type: string
            format: date
          description: Inclusive upper bound on ``proc_date`` (ISO 8601 date).
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: string
            format: uuid
          description: Filter by provider UUID.
        - in: query
          name: search
          schema:
            type: string
          description: >-
            Case-insensitive substring search across ``proc_code`` and
            ``description``.
        - in: query
          name: status
          schema:
            type: string
            enum:
              - complete
              - existing_current
              - existing_other
          description: >-
            Filter by procedure status. Common values are ``complete``,
            ``existing_current``, and ``existing_other``; free-text is accepted
            for legacy PMS imports.
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedProcedureList'
          description: ''
    post:
      operationId: practice_patient_clinical_procedure_create
      description: >-
        List and create procedures for a patient.


        Supports filtering by provider, date range, status, and free-text
        search.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProcedureWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ProcedureWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ProcedureWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProcedureWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_clinical_procedure_bulk_partial_update
      description: Handle PATCH on the list endpoint for bulk updates.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedProcedureRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedProcedureRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedProcedureRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Procedure'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/procedure/{procedure_uid}/:
    get:
      operationId: practice_patient_clinical_procedure_retrieve
      description: Retrieve, update, or delete a procedure.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: procedure_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Procedure'
          description: ''
    put:
      operationId: practice_patient_clinical_procedure_update
      description: Retrieve, update, or delete a procedure.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: procedure_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProcedureWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ProcedureWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ProcedureWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProcedureWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_clinical_procedure_partial_update
      description: Retrieve, update, or delete a procedure.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: procedure_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedProcedureWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedProcedureWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedProcedureWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProcedureWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_patient_clinical_procedure_destroy
      description: Retrieve, update, or delete a procedure.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: procedure_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/timeline/:
    get:
      operationId: practice_patient_clinical_timeline_retrieve
      description: |-
        Unified chronological feed for a patient.

        Combines procedures, clinical notes, perio exams, treatment plans,
        imaging exams, communication logs, and appointments into a single
        sorted timeline.  Paginated, newest first.

        Accepts an optional ``event_type`` query parameter (comma-separated)
        to filter to specific event types.
      parameters:
        - in: query
          name: event_type
          schema:
            type: string
          description: >-
            Comma-separated list of event types to include. Valid values:
            procedure, note, perio_exam, treatment_plan, imaging, communication,
            appointment. Omit to include all types.
        - in: query
          name: page
          schema:
            type: integer
          description: Page number (1-indexed)
        - in: query
          name: page_size
          schema:
            type: integer
          description: Number of events per page (default 50, max 200)
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/tooth/:
    get:
      operationId: practice_patient_clinical_tooth_list
      description: |-
        List and create tooth conditions for a patient.

        Defaults to current conditions only (is_current=True).
        Supports filtering by tooth_num and overriding is_current.
      parameters:
        - in: query
          name: is_current
          schema:
            type: string
            enum:
              - all
              - 'false'
          description: >-
            Filter by current status. Defaults to current-only. Accepts
            ``false`` to return non-current conditions, or ``all`` to return
            every condition.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: tooth_num
          schema:
            type: string
          description: Filter by tooth number (Universal Numbering System, e.g. ``14``).
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedToothConditionList'
          description: ''
    post:
      operationId: practice_patient_clinical_tooth_create
      description: |-
        List and create tooth conditions for a patient.

        Defaults to current conditions only (is_current=True).
        Supports filtering by tooth_num and overriding is_current.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ToothConditionRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ToothConditionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ToothConditionRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToothCondition'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/tooth/{tooth_uid}/:
    get:
      operationId: practice_patient_clinical_tooth_retrieve
      description: Retrieve, update, or delete a tooth condition.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: tooth_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToothCondition'
          description: ''
    put:
      operationId: practice_patient_clinical_tooth_update
      description: Retrieve, update, or delete a tooth condition.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: tooth_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ToothConditionRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ToothConditionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ToothConditionRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToothCondition'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_clinical_tooth_partial_update
      description: Retrieve, update, or delete a tooth condition.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: tooth_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedToothConditionRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedToothConditionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedToothConditionRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToothCondition'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_patient_clinical_tooth_destroy
      description: Retrieve, update, or delete a tooth condition.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: tooth_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/tooth/summary/:
    get:
      operationId: practice_patient_clinical_tooth_summary_retrieve
      description: |-
        Aggregated tooth chart for the odontogram.

        Returns all current conditions grouped by tooth number.
        Response: {"teeth": {"1": [conditions...], "14": [conditions...]}}
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/treatment-plan/:
    get:
      operationId: practice_patient_clinical_treatment_plan_list
      description: |-
        List and create treatment plans for a patient.

        Supports filtering by status.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: status
          schema:
            type: string
            enum:
              - accepted
              - active
              - archived
              - completed
              - declined
          description: Filter by treatment plan status.
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedTreatmentPlanList'
          description: ''
    post:
      operationId: practice_patient_clinical_treatment_plan_create
      description: |-
        List and create treatment plans for a patient.

        Supports filtering by status.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TreatmentPlanWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/TreatmentPlanWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/TreatmentPlanWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TreatmentPlanWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/treatment-plan/{plan_uid}/:
    get:
      operationId: practice_patient_clinical_treatment_plan_retrieve
      description: Retrieve, update, or delete a treatment plan.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TreatmentPlan'
          description: ''
    put:
      operationId: practice_patient_clinical_treatment_plan_update
      description: Retrieve, update, or delete a treatment plan.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TreatmentPlanWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/TreatmentPlanWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/TreatmentPlanWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TreatmentPlanWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_clinical_treatment_plan_partial_update
      description: Retrieve, update, or delete a treatment plan.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedTreatmentPlanWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedTreatmentPlanWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedTreatmentPlanWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TreatmentPlanWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_patient_clinical_treatment_plan_destroy
      description: Retrieve, update, or delete a treatment plan.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/treatment-plan/{plan_uid}/accept/:
    post:
      operationId: practice_patient_clinical_treatment_plan_accept_create
      description: Accept a treatment plan. Sets status=accepted and date_accepted=today.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/treatment-plan/{plan_uid}/decline/:
    post:
      operationId: practice_patient_clinical_treatment_plan_decline_create
      description: Decline a treatment plan. Sets status=declined.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/treatment-plan/{plan_uid}/estimate/:
    post:
      operationId: practice_patient_clinical_treatment_plan_estimate_create
      description: >-
        Estimate insurance benefits for all items in a treatment plan.


        Calls the insurance estimation service with all plan items as a batch so

        deductible and annual max accumulate correctly across lines. Persists
        the

        results back to each TreatmentPlanItem and returns the updated plan.


        POST /patient/{patient_id}/clinical/treatment-plan/{pk}/estimate/

        Optional body: {"explain": true} for step-by-step calculation details.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/treatment-plan/{plan_uid}/item/:
    get:
      operationId: practice_patient_clinical_treatment_plan_item_list
      description: List and create items within a treatment plan.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedTreatmentPlanItemList'
          description: ''
    post:
      operationId: practice_patient_clinical_treatment_plan_item_create
      description: List and create items within a treatment plan.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TreatmentPlanItemRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/TreatmentPlanItemRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/TreatmentPlanItemRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TreatmentPlanItem'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/treatment-plan/{plan_uid}/item/{item_uid}/:
    get:
      operationId: practice_patient_clinical_treatment_plan_item_retrieve
      description: Retrieve, update, or delete a treatment plan item.
      parameters:
        - in: path
          name: item_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TreatmentPlanItem'
          description: ''
    put:
      operationId: practice_patient_clinical_treatment_plan_item_update
      description: Retrieve, update, or delete a treatment plan item.
      parameters:
        - in: path
          name: item_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TreatmentPlanItemRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/TreatmentPlanItemRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/TreatmentPlanItemRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TreatmentPlanItem'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_clinical_treatment_plan_item_partial_update
      description: Retrieve, update, or delete a treatment plan item.
      parameters:
        - in: path
          name: item_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedTreatmentPlanItemRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedTreatmentPlanItemRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedTreatmentPlanItemRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TreatmentPlanItem'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_patient_clinical_treatment_plan_item_destroy
      description: Retrieve, update, or delete a treatment plan item.
      parameters:
        - in: path
          name: item_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/treatment-plan/{plan_uid}/schedule/:
    post:
      operationId: practice_patient_clinical_treatment_plan_schedule_create
      description: >-
        Create an appointment from treatment plan items.


        POST accepts: provider, operatory, date_time, duration_minutes, item_ids
        (optional).

        If item_ids is omitted, all plan items are scheduled.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/vital-sign/:
    get:
      operationId: practice_patient_clinical_vital_sign_list
      description: |-
        List and create vital sign readings for a patient.

        Supports filtering by appointment and date range.
        Ordered by most recent reading first.
      parameters:
        - in: query
          name: appointment
          schema:
            type: string
            format: uuid
          description: Filter readings to a single appointment UUID.
        - in: query
          name: date_from
          schema:
            type: string
            format: date
          description: >-
            Inclusive lower bound on ``reading_date`` (ISO 8601 date).
            Interpreted in the practice's local timezone.
        - in: query
          name: date_to
          schema:
            type: string
            format: date
          description: >-
            Inclusive upper bound on ``reading_date`` (ISO 8601 date).
            Interpreted in the practice's local timezone.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedVitalSignList'
          description: ''
    post:
      operationId: practice_patient_clinical_vital_sign_create
      description: |-
        List and create vital sign readings for a patient.

        Supports filtering by appointment and date range.
        Ordered by most recent reading first.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Clinical
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VitalSignWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/VitalSignWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/VitalSignWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VitalSignWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/clinical/vital-sign/{vital_sign_uid}/:
    get:
      operationId: practice_patient_clinical_vital_sign_retrieve
      description: Retrieve or update a vital sign reading.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: vital_sign_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VitalSign'
          description: ''
    put:
      operationId: practice_patient_clinical_vital_sign_update
      description: Retrieve or update a vital sign reading.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: vital_sign_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VitalSignWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/VitalSignWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/VitalSignWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VitalSignWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_clinical_vital_sign_partial_update
      description: Retrieve or update a vital sign reading.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: vital_sign_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedVitalSignWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedVitalSignWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedVitalSignWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VitalSignWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/comms/commlog/:
    get:
      operationId: practice_patient_comms_commlog_list
      description: >-
        List and create communication log entries for a patient.


        Supports ``?date_from=`` and ``?date_to=`` query params for date
        filtering.

        Bulk create is supported by POSTing an array.


        GH-575: every list GET emits one ``comms.commlog_listed`` audit event

        carrying the actor, patient, and result count.  Per-row events would

        N+1 the audit index on a 50-row page so we only fire once per request.
      parameters:
        - in: query
          name: date_from
          schema:
            type: string
            format: date
          description: >-
            Inclusive lower bound on ``date_time`` (``YYYY-MM-DD``).  Filters
            against the practice-local date portion of the comm log's timestamp.
        - in: query
          name: date_to
          schema:
            type: string
            format: date
          description: >-
            Inclusive upper bound on ``date_time`` (``YYYY-MM-DD``).  Filters
            against the practice-local date portion of the comm log's timestamp.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedCommLogList'
          description: ''
    post:
      operationId: practice_patient_comms_commlog_create
      description: >-
        List and create communication log entries for a patient.


        Supports ``?date_from=`` and ``?date_to=`` query params for date
        filtering.

        Bulk create is supported by POSTing an array.


        GH-575: every list GET emits one ``comms.commlog_listed`` audit event

        carrying the actor, patient, and result count.  Per-row events would

        N+1 the audit index on a 50-row page so we only fire once per request.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommLogWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/CommLogWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/CommLogWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommLogWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_comms_commlog_bulk_partial_update
      description: Handle PATCH on the list endpoint for bulk updates.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedCommLogRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedCommLogRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedCommLogRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommLog'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/comms/commlog/{commlog_uid}/:
    get:
      operationId: practice_patient_comms_commlog_retrieve
      description: |-
        Retrieve, update, or delete a communication log entry.

        GH-575: every detail GET emits one ``comms.commlog_viewed`` audit
        event carrying ``is_imported`` so HIPAA review can filter reads of
        PHI-bearing historical imports specifically.

        TODO(GH-575): when a ``replicate_of_commlog_id`` send endpoint is
        added (e.g. "resend this email" / "use as template"), it MUST 400
        when the source row is ``is_imported=True`` and emit
        ``comms.commlog_resend_blocked`` to the audit trail.  Follow-up
        issue tracked in escapedental/web (suppress the affordance UI-side)
        and in api (when the send endpoint actually lands).
      parameters:
        - in: path
          name: commlog_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommLog'
          description: ''
    put:
      operationId: practice_patient_comms_commlog_update
      description: |-
        Retrieve, update, or delete a communication log entry.

        GH-575: every detail GET emits one ``comms.commlog_viewed`` audit
        event carrying ``is_imported`` so HIPAA review can filter reads of
        PHI-bearing historical imports specifically.

        TODO(GH-575): when a ``replicate_of_commlog_id`` send endpoint is
        added (e.g. "resend this email" / "use as template"), it MUST 400
        when the source row is ``is_imported=True`` and emit
        ``comms.commlog_resend_blocked`` to the audit trail.  Follow-up
        issue tracked in escapedental/web (suppress the affordance UI-side)
        and in api (when the send endpoint actually lands).
      parameters:
        - in: path
          name: commlog_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommLogWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/CommLogWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/CommLogWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommLogWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_comms_commlog_partial_update
      description: |-
        Retrieve, update, or delete a communication log entry.

        GH-575: every detail GET emits one ``comms.commlog_viewed`` audit
        event carrying ``is_imported`` so HIPAA review can filter reads of
        PHI-bearing historical imports specifically.

        TODO(GH-575): when a ``replicate_of_commlog_id`` send endpoint is
        added (e.g. "resend this email" / "use as template"), it MUST 400
        when the source row is ``is_imported=True`` and emit
        ``comms.commlog_resend_blocked`` to the audit trail.  Follow-up
        issue tracked in escapedental/web (suppress the affordance UI-side)
        and in api (when the send endpoint actually lands).
      parameters:
        - in: path
          name: commlog_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedCommLogWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedCommLogWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedCommLogWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommLogWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_patient_comms_commlog_destroy
      description: |-
        Retrieve, update, or delete a communication log entry.

        GH-575: every detail GET emits one ``comms.commlog_viewed`` audit
        event carrying ``is_imported`` so HIPAA review can filter reads of
        PHI-bearing historical imports specifically.

        TODO(GH-575): when a ``replicate_of_commlog_id`` send endpoint is
        added (e.g. "resend this email" / "use as template"), it MUST 400
        when the source row is ``is_imported=True`` and emit
        ``comms.commlog_resend_blocked`` to the audit trail.  Follow-up
        issue tracked in escapedental/web (suppress the affordance UI-side)
        and in api (when the send endpoint actually lands).
      parameters:
        - in: path
          name: commlog_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/doccenter/:
    get:
      operationId: practice_patient_doccenter_list
      description: List documents for a specific patient. Filter by ?category=.
      parameters:
        - in: query
          name: category
          schema:
            type: string
            format: uuid
          description: >-
            Filter patient documents by category UID.  Categories are returned
            by ``GET /api/v1/practice/{practice_uid}/doccenter/category/``.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedDocumentList'
          description: ''
  /api/v1/practice/{practice_uid}/patient/{patient_id}/doccenter/{doc_id}/:
    get:
      operationId: practice_patient_doccenter_retrieve
      description: Retrieve or update document metadata.
      parameters:
        - in: path
          name: doc_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Document'
          description: ''
    put:
      operationId: practice_patient_doccenter_update
      description: Retrieve or update document metadata.
      parameters:
        - in: path
          name: doc_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentUpdateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/DocumentUpdateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/DocumentUpdateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentUpdate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_doccenter_partial_update
      description: Retrieve or update document metadata.
      parameters:
        - in: path
          name: doc_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedDocumentUpdateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedDocumentUpdateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedDocumentUpdateRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentUpdate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/doccenter/{doc_id}/delete/:
    delete:
      operationId: practice_patient_doccenter_delete_destroy
      description: Soft-delete a document (HIPAA retention).
      parameters:
        - in: path
          name: doc_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/doccenter/upload/:
    post:
      operationId: practice_patient_doccenter_upload_create
      description: Upload a document associated with a patient.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Documents
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/forms/{template_id}/prefill/:
    get:
      operationId: practice_patient_forms_prefill_retrieve
      description: >-
        ``GET
        /api/v1/practice/<uid>/patient/<patient_id>/forms/<template_id>/prefill/``


        Resolve a form template's :attr:`FormTemplate.prefill_map` against a

        specific patient and return the values the kiosk / portal should

        pre-populate (api#772).


        Auth

        ~~~~

        Two acceptable callers:


        * **Staff session** -- gated to manager-or-above through
          :class:`IsManagerOrAbove`.  Same convention as the bundle
          endpoints: pre-fill data is PHI (patient demographics + insurance
          identifiers) so front-desk-only roles don't get it.
        * **Paired tablet bearer** -- a tablet whose
          :attr:`apps.tablets.models.Tablet.practice` matches the URL
          practice may hit the endpoint.  This is the kiosk path: the
          tablet renderer asks for pre-fill values before showing the
          patient the form.

        A request authenticated as a tablet paired with a DIFFERENT

        practice is rejected with 403 -- the URL practice is the source

        of truth and a paired-elsewhere tablet must not see another

        office's patient data even with a valid bearer.


        Response shape

        ~~~~~~~~~~~~~~

        .. code:: json

            {
              "template_id": "...",
              "values": {
                "First name": "Jane",
                "Date of birth": "1985-05-22"
              }
            }

        Empty ``prefill_map``, missing patient attributes, and null values

        all surface as an empty / shorter ``values`` dict -- the renderer

        treats absent keys as "user must fill in" so the resolver is

        permissively quiet.


        Errors

        ~~~~~~

        * 404 when the template or patient doesn't exist in this practice.

        * 403 when a tablet bearer is paired with a different practice or
          a staff caller is below manager rank.
        * 401 when no authentication credentials are supplied at all.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Forms
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/practice/{practice_uid}/patient/{patient_id}/imaging/exam/:
    get:
      operationId: practice_patient_imaging_exam_list
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedExamList'
          description: ''
    post:
      operationId: practice_patient_imaging_exam_create
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExamRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ExamRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ExamRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Exam'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/imaging/exam/{exam_uid}/:
    get:
      operationId: practice_patient_imaging_exam_retrieve
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: exam_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExamDetail'
          description: ''
    put:
      operationId: practice_patient_imaging_exam_update
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: exam_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExamRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ExamRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ExamRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Exam'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_imaging_exam_partial_update
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: exam_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedExamRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedExamRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedExamRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Exam'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_patient_imaging_exam_destroy
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: exam_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/imaging/image/:
    get:
      operationId: practice_patient_imaging_image_list
      description: |-
        List images for a patient with filtering and ordering.

        Query params:
            image_type   — filter by type (PA, BW, PANO, CEPH, etc.)
            source       — filter by source ID
            exam         — filter by exam ID
            date_from    — capture_date >= (YYYY-MM-DD)
            date_to      — capture_date <= (YYYY-MM-DD)
            ordering     — comma-separated fields: capture_date, created, image_type
                           (prefix with ``-`` for descending, default: -capture_date,-created)
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedExamImageList'
          description: ''
  /api/v1/practice/{practice_uid}/patient/{patient_id}/imaging/image/{image_uid}/:
    get:
      operationId: practice_patient_imaging_image_retrieve
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: image_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExamImage'
          description: ''
    put:
      operationId: practice_patient_imaging_image_update
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: image_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExamImageRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ExamImageRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ExamImageRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExamImage'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_imaging_image_partial_update
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: image_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedExamImageRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedExamImageRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedExamImageRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExamImage'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_patient_imaging_image_destroy
      description: |-
        Resolve ``practice_uid`` from the URL and enforce client-level access.

        Mixed-in views automatically pick up
        :class:`apps.practices.permissions.PracticeIsActive` so writes to
        descendants of an inactive Client / Practice are blocked with a 403
        even if the view overrides ``permission_classes`` *or*
        ``get_permissions()``.  See GH-277 for the cascade-disable
        rationale.
      parameters:
        - in: path
          name: image_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/imaging/image/upload/:
    post:
      operationId: practice_patient_imaging_image_upload_create
      description: Upload a dental image via multipart form data.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ExamImageUploadRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ExamImageUploadRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExamImageUpload'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/insurance/claim/:
    get:
      operationId: practice_patient_insurance_claim_list
      description: >-
        Insurance claims for a patient. POST to create a new claim in draft
        status.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: status
          schema:
            type: string
            enum:
              - accepted
              - appealed
              - denied
              - draft
              - imported
              - paid
              - partial
              - rejected
              - resubmitted
              - submitted
              - void
          description: Filter claims by their current state-machine status.
      tags:
        - Insurance
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedInsuranceClaimList'
          description: ''
    post:
      operationId: practice_patient_insurance_claim_create
      description: >-
        Insurance claims for a patient. POST to create a new claim in draft
        status.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InsuranceClaimWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/InsuranceClaimWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/InsuranceClaimWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceClaimWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/insurance/claim/{claim_id}/log/:
    get:
      operationId: practice_patient_insurance_claim_log_list
      description: |-
        List status change log entries for a specific claim.

        GET /practice/<uid>/patient/<patient_id>/insurance/claim/<claim_id>/log/
      parameters:
        - in: path
          name: claim_id
          schema:
            type: string
            format: uuid
          required: true
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedClaimStatusLogList'
          description: ''
  /api/v1/practice/{practice_uid}/patient/{patient_id}/insurance/claim/{claim_uid}/:
    get:
      operationId: practice_patient_insurance_claim_retrieve
      description: |-
        Retrieve a single insurance claim with full details.

        GET /practice/<uid>/patient/<patient_id>/insurance/claim/<claim_uid>/
      parameters:
        - in: path
          name: claim_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceClaim'
          description: ''
  /api/v1/practice/{practice_uid}/patient/{patient_id}/insurance/claim/{claim_uid}/transition/:
    post:
      operationId: practice_patient_insurance_claim_transition_create
      description: >-
        Transition a claim's status through the state machine.


        POST
        /practice/<uid>/patient/<patient_id>/insurance/claim/<claim_uid>/transition/

        Body: {"new_status": "submitted", "note": "Sent to Delta Dental"}
      parameters:
        - in: path
          name: claim_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClaimTransitionRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ClaimTransitionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ClaimTransitionRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClaimTransition'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/insurance/claim/{claim_uid}/update/:
    patch:
      operationId: practice_patient_insurance_claim_update_partial_update
      description: |-
        Update an insurance claim. Only draft or imported claims may be edited.

        PATCH /practice/<uid>/patient/<patient_id>/insurance/claim/<claim_uid>/
      parameters:
        - in: path
          name: claim_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedInsuranceClaimWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedInsuranceClaimWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedInsuranceClaimWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceClaimWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/insurance/estimate/:
    post:
      operationId: practice_patient_insurance_estimate_create
      description: |-
        Estimate insurance coverage for proposed procedures.

        POST /practice/{uid}/patient/{patient_id}/insurance/estimate/
        Body: { "procedures": [{ "proc_code": "D2391", "fee": "450.00" }] }
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/insurance/estimate-explain/:
    post:
      operationId: practice_patient_insurance_estimate_explain_create
      description: |-
        Verbose step-by-step insurance estimation with explanation.

        POST /practice/{uid}/patient/{patient_id}/insurance/estimate-explain/
        Body: { "procedures": [{ "proc_code": "D2391", "fee": "450.00" }] }

        Returns the same structure as the standard estimate endpoint, plus an
        'explanation' key containing global and per-line calculation steps.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/insurance/plan/:
    get:
      operationId: practice_patient_insurance_plan_list
      description: >-
        List a patient's insurance enrollments (``PatientInsurance`` rows). 
        Scope is fixed by the ``patient_id`` path parameter; the filter set is
        intentionally empty -- use the practice-wide endpoint
        (``/insurance/patient-insurance/``) if cross-patient filtering is
        required.  Standard ``page`` / ``page_size`` pagination applies.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPatientInsuranceList'
          description: ''
    post:
      operationId: practice_patient_insurance_plan_create
      description: List and create insurance plans for a patient.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientInsuranceWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientInsuranceWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientInsuranceWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientInsuranceWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/insurance/plan/{plan_id}/benefit/:
    get:
      operationId: practice_patient_insurance_plan_benefit_list
      description: >-
        List benefits attached to a patient's insurance enrollment.  Scope is
        fixed by the ``patient_id`` and ``plan_id`` path parameters; the filter
        set is intentionally empty -- all relevant narrowing is already
        expressed by the URL.  Standard ``page`` / ``page_size`` pagination
        applies.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedInsuranceBenefitList'
          description: ''
    post:
      operationId: practice_patient_insurance_plan_benefit_create
      description: List and create benefits for a patient insurance plan.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Insurance
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InsuranceBenefitWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/InsuranceBenefitWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/InsuranceBenefitWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceBenefitWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/insurance/plan/{plan_id}/benefit/{benefit_uid}/:
    get:
      operationId: practice_patient_insurance_plan_benefit_retrieve
      description: Update or delete a benefit.
      parameters:
        - in: path
          name: benefit_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceBenefit'
          description: ''
    put:
      operationId: practice_patient_insurance_plan_benefit_update
      description: Update or delete a benefit.
      parameters:
        - in: path
          name: benefit_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InsuranceBenefitWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/InsuranceBenefitWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/InsuranceBenefitWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceBenefitWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_insurance_plan_benefit_partial_update
      description: Update or delete a benefit.
      parameters:
        - in: path
          name: benefit_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedInsuranceBenefitWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedInsuranceBenefitWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedInsuranceBenefitWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsuranceBenefitWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_patient_insurance_plan_benefit_destroy
      description: Update or delete a benefit.
      parameters:
        - in: path
          name: benefit_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/insurance/plan/{plan_uid}/:
    get:
      operationId: practice_patient_insurance_plan_retrieve
      description: Retrieve, update, or delete a patient insurance plan.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientInsurance'
          description: ''
    put:
      operationId: practice_patient_insurance_plan_update
      description: Retrieve, update, or delete a patient insurance plan.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientInsuranceWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientInsuranceWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientInsuranceWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientInsuranceWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_insurance_plan_partial_update
      description: Retrieve, update, or delete a patient insurance plan.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPatientInsuranceWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPatientInsuranceWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPatientInsuranceWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientInsuranceWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_patient_insurance_plan_destroy
      description: Retrieve, update, or delete a patient insurance plan.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: plan_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/prescription/:
    get:
      operationId: practice_patient_prescription_list
      description: |-
        List and create prescriptions.

        ``GET``  /v1/practice/<uid>/prescription/
        ``POST`` /v1/practice/<uid>/prescription/

        Query params
        ------------
        ``patient`` — filter by patient UUID
        ``status``  — filter by status (draft/final/cancelled)
        ``search``  — case-insensitive drug name search
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPrescriptionList'
          description: ''
    post:
      operationId: practice_patient_prescription_create
      description: |-
        List and create prescriptions.

        ``GET``  /v1/practice/<uid>/prescription/
        ``POST`` /v1/practice/<uid>/prescription/

        Query params
        ------------
        ``patient`` — filter by patient UUID
        ``status``  — filter by status (draft/final/cancelled)
        ``search``  — case-insensitive drug name search
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PrescriptionRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PrescriptionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PrescriptionRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Prescription'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/prescription/{prescription_id}/:
    get:
      operationId: practice_patient_prescription_retrieve
      description: |-
        Retrieve or update a prescription (draft only).

        ``GET``   /v1/practice/<uid>/prescription/<id>/
        ``PATCH`` /v1/practice/<uid>/prescription/<id>/

        Only ``draft`` prescriptions may be edited.  Use the ``/finalise/``
        action to advance to ``final``.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: prescription_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Prescription'
          description: ''
    put:
      operationId: practice_patient_prescription_update
      description: |-
        Retrieve or update a prescription (draft only).

        ``GET``   /v1/practice/<uid>/prescription/<id>/
        ``PATCH`` /v1/practice/<uid>/prescription/<id>/

        Only ``draft`` prescriptions may be edited.  Use the ``/finalise/``
        action to advance to ``final``.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: prescription_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PrescriptionRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PrescriptionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PrescriptionRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Prescription'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_prescription_partial_update
      description: |-
        Retrieve or update a prescription (draft only).

        ``GET``   /v1/practice/<uid>/prescription/<id>/
        ``PATCH`` /v1/practice/<uid>/prescription/<id>/

        Only ``draft`` prescriptions may be edited.  Use the ``/finalise/``
        action to advance to ``final``.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: prescription_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPrescriptionRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPrescriptionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPrescriptionRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Prescription'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/prescription/{prescription_id}/cancel/:
    post:
      operationId: practice_patient_prescription_cancel_create
      description: >-
        Cancel a prescription (draft or final).


        ``POST`` /v1/practice/<uid>/prescription/<id>/cancel/


        Any staff member may cancel a draft.  Only ADMIN/PROVIDER may cancel

        a finalised prescription (e.g. if an error was discovered after
        printing).
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: prescription_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/prescription/{prescription_id}/finalise/:
    post:
      operationId: practice_patient_prescription_finalise_create
      description: |-
        Finalise a draft prescription.

        ``POST`` /v1/practice/<uid>/prescription/<id>/finalise/

        Only a user who holds a prescribing-provider role (ADMIN or PROVIDER in
        the practice membership) may finalise.  This represents the provider
        reviewing the draft entered by staff and giving their sign-off.

        For controlled substances, the provider must still physically sign the
        *printed* paper prescription.  This endpoint is NOT an EPCS action and
        does not constitute an electronic DEA signature.

        Returns the updated prescription payload.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: prescription_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_id}/prescription/{prescription_id}/pdf/:
    get:
      operationId: practice_patient_prescription_pdf_retrieve
      description: |-
        Render a prescription as a printable PDF.

        ``GET`` /v1/practice/<uid>/prescription/<id>/pdf/

        Returns ``application/pdf``.  Only ``final`` prescriptions may be
        rendered; draft prescriptions return 400.

        The PDF is formatted as a standard paper Rx form including:
        - Practice name, address, phone
        - Provider name, NPI, DEA number
        - Patient name, date of birth
        - Date of prescription
        - Drug details and directions
        - Blank diagonal line below the drug entry (tamper-evident, required
          for controlled substance paper prescriptions in many states)
        - Provider signature line

        Emits a ``prescription.printed`` audit event so there is a permanent
        record of when the form was accessed for printing.

        **Note**: For paper controlled-substance prescriptions, the provider
        must physically sign the printed form.  This endpoint is not an EPCS
        action and does not constitute a DEA electronic signature.
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: prescription_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/pdf:
              schema:
                type: string
                format: binary
          description: ''
  /api/v1/practice/{practice_uid}/patient/{patient_id}/scheduling/recall/:
    get:
      operationId: practice_patient_scheduling_recall_list
      description: List recalls for a specific patient (patient-scoped, read-only).
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedRecallList'
          description: ''
  /api/v1/practice/{practice_uid}/patient/{patient_id}/sms-consent/:
    get:
      operationId: practice_patient_sms_consent_retrieve
      description: |-
        Convenience single-resource view for the patient's consent row.

        Returns 404 when no row exists yet.  The frontend (web#485) treats
        a 404 as "default opted_in, no row yet" -- this matches the lazy-
        create semantics of :class:`SMSConsent` (the row only materialises
        once a STOP / START / first outbound hits the consent gate).

        No GET-side row creation: a stray check from the staff UI should
        not leave an audit-noise OPTED_IN row behind on every patient
        profile the user happens to visit.
      summary: Get the SMS consent row for a single patient
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SMSConsent'
          description: ''
  /api/v1/practice/{practice_uid}/patient/{patient_id}/sms-consent/force-opt-in/:
    post:
      operationId: practice_patient_sms_consent_force_opt_in_create
      description: >-
        Staff-initiated SMS re-opt-in for a single (practice, patient) row.


        CTIA / 10DLC permits manual re-opt-in only when the patient asks

        for it, so this endpoint records ``review_notes`` from the staff

        operator alongside the standard ``actor`` capture in the audit

        document.  Idempotent on an already-opted-in row -- the staff UI

        can call it without first checking state and the audit trail still

        captures the request (so a misclick is reviewable).


        Behaviour:


        * If no consent row exists, creates one in OPTED_IN.

        * If OPTED_OUT, flips to OPTED_IN and clears opt-out audit columns.

        * If already OPTED_IN, updates ``review_notes`` + ``last_change_by``
          and re-emits the audit event so the override is on record.

        Emits ``sms.opted_in`` with ``metadata={"source":
        "staff_force_opt_in"}``

        on every call.
      summary: Staff force-opt-in override for a patient's SMS consent
      parameters:
        - in: path
          name: patient_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SMSConsentForceOptInRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/SMSConsentForceOptInRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/SMSConsentForceOptInRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SMSConsent'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/{patient_uid}/:
    get:
      operationId: practice_patient_retrieve
      description: Retrieve or update a single patient. Staff-only.
      parameters:
        - in: path
          name: patient_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Patient'
          description: ''
    put:
      operationId: practice_patient_update
      description: Retrieve or update a single patient. Staff-only.
      parameters:
        - in: path
          name: patient_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Patient'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_partial_update
      description: Retrieve or update a single patient. Staff-only.
      parameters:
        - in: path
          name: patient_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPatientRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPatientRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPatientRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Patient'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/referral-source/:
    get:
      operationId: practice_patient_referral_source_list
      description: |-
        List and create referral sources for a practice.

        Bulk create is supported by POSTing an array.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedReferralSourceList'
          description: ''
    post:
      operationId: practice_patient_referral_source_create
      description: |-
        List and create referral sources for a practice.

        Bulk create is supported by POSTing an array.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReferralSourceWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ReferralSourceWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ReferralSourceWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferralSourceWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_referral_source_bulk_partial_update
      description: Handle PATCH on the list endpoint for bulk updates.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Patients
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedReferralSourceRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedReferralSourceRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedReferralSourceRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferralSource'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/referral-source/{referral_source_uid}/:
    get:
      operationId: practice_patient_referral_source_retrieve
      description: Retrieve, update, or delete a referral source.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: referral_source_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferralSource'
          description: ''
    put:
      operationId: practice_patient_referral_source_update
      description: Retrieve, update, or delete a referral source.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: referral_source_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReferralSourceWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ReferralSourceWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ReferralSourceWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferralSourceWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_patient_referral_source_partial_update
      description: Retrieve, update, or delete a referral source.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: referral_source_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedReferralSourceWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedReferralSourceWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedReferralSourceWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferralSourceWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_patient_referral_source_destroy
      description: Retrieve, update, or delete a referral source.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: referral_source_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/patient/stats/:
    get:
      operationId: practice_patient_stats_retrieve
      description: Quick stats for the practice's patient data. Staff-only.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Patients
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/practice/{practice_uid}/portal/appointment/book/:
    post:
      operationId: practice_portal_appointment_book_create
      description: >-
        Book an appointment as an authenticated patient.


        POST /api/v1/practice/<uid>/portal/appointment/book/


        Only patient users (is_patient=True) may access this endpoint. The
        patient

        is automatically resolved from the authenticated user's email. Provider
        and

        operatory are validated against the practice. Conflicts are checked
        before

        creating the appointment.


        Returns 201 with the created appointment on success, or 409 with
        conflict

        details if the requested time slot is unavailable.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientBookingRequest'
            examples:
              BookAHygieneVisit:
                value:
                  provider: 22222222-2222-4222-8222-222222222222
                  operatory: 44444444-4444-4444-8444-444444444444
                  start_time: '2026-05-20T10:00:00-07:00'
                  duration_minutes: 60
                  procedures:
                    - D1110
                  notes: First availability in the morning, please.
                summary: Patient books a 60-minute cleaning
                description: >-
                  Minimum payload to book an appointment from the patient
                  portal.  ``start_time`` MUST include a timezone offset --
                  naive datetimes are rejected with a 400.  ``procedures`` is
                  optional; if provided, CDT codes are matched against the
                  practice's procedure table.
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientBookingRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientBookingRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientBookingResponse'
              examples:
                BookingConfirmed:
                  value:
                    id: 55555555-5555-4555-8555-555555555555
                    provider: 22222222-2222-4222-8222-222222222222
                    operatory: 44444444-4444-4444-8444-444444444444
                    date_time: '2026-05-20T10:00:00-07:00'
                    duration_minutes: 60
                    status: scheduled
                    note: First availability in the morning, please.
                    provider_name: Doe, Dr. John
                  summary: 201 Created -- appointment scheduled
                  description: >-
                    Returned after the portal successfully books an appointment
                    on the patient's behalf.
          description: ''
        '400':
          description: Validation error (e.g. naive datetime, past start_time).
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: No patient record found for this portal user in the practice.
        '409':
          description: Time slot is not available.
  /api/v1/practice/{practice_uid}/portal/appointment/validate/:
    post:
      operationId: practice_portal_appointment_validate_create
      description: |-
        Pre-flight conflict check for a proposed patient appointment.

        POST /api/v1/practice/<uid>/portal/appointment/validate/

        Same payload as the booking endpoint but only checks for conflicts
        without creating anything. Returns 200 with ``{"available": true}``
        or 409 with conflict details.

        Only patient users (is_patient=True) may access this endpoint.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientBookingRequest'
            examples:
              BookAHygieneVisit:
                value:
                  provider: 22222222-2222-4222-8222-222222222222
                  operatory: 44444444-4444-4444-8444-444444444444
                  start_time: '2026-05-20T10:00:00-07:00'
                  duration_minutes: 60
                  procedures:
                    - D1110
                  notes: First availability in the morning, please.
                summary: Patient books a 60-minute cleaning
                description: >-
                  Minimum payload to book an appointment from the patient
                  portal.  ``start_time`` MUST include a timezone offset --
                  naive datetimes are rejected with a 400.  ``procedures`` is
                  optional; if provided, CDT codes are matched against the
                  practice's procedure table.
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientBookingRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientBookingRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: Slot is available.
        '400':
          description: Validation error (e.g. naive datetime).
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '409':
          description: Slot has a conflict.
  /api/v1/practice/{practice_uid}/portal/appointments/{appointment_uid}/cancel/:
    post:
      operationId: practice_portal_appointments_cancel_create
      description: |-
        Cancel an appointment as an authenticated patient.

        POST /api/v1/practice/<uid>/portal/appointments/<id>/cancel/

        Only patient users (is_patient=True) may access this endpoint. The
        patient record is resolved from the authenticated user's email and
        must match the appointment's patient. Only appointments in a
        cancellable state (currently "scheduled") may be cancelled.

        An audit-trail ``AppointmentStatusLog`` entry is created and an audit
        event is emitted for HIPAA compliance.

        Returns 200 with the updated appointment on success.
      parameters:
        - in: path
          name: appointment_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientCancellationRequest'
            examples:
              CancelWithRescheduleRequest:
                value:
                  reason: Out of town for work that week.
                  reschedule_request: true
                summary: Cancel an appointment, ask the front desk to reach out
                description: >-
                  Both fields are optional.  ``reschedule_request=true``
                  notifies the front desk to follow up with the patient so the
                  slot can be re-filled and the patient re-booked.
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientCancellationRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientCancellationRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientCancellationResponse'
              examples:
                CancellationConfirmed:
                  value:
                    id: 55555555-5555-4555-8555-555555555555
                    provider: 22222222-2222-4222-8222-222222222222
                    operatory: 44444444-4444-4444-8444-444444444444
                    date_time: '2026-05-20T10:00:00-07:00'
                    duration_minutes: 60
                    status: cancelled
                    cancellation_reason: Out of town for work that week.
                    reschedule_requested: true
                    provider_name: Doe, Dr. John
                  summary: 200 OK -- appointment cancelled
                  description: >-
                    Returns the updated appointment with ``status=cancelled``. 
                    An ``AppointmentStatusLog`` entry is created server-side for
                    the HIPAA audit trail.
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Appointment not found or patient record missing.
        '409':
          description: Appointment is not in a cancellable state.
  /api/v1/practice/{practice_uid}/portal/available-slots/:
    get:
      operationId: practice_portal_available_slots_retrieve
      description: |-
        Single-day slot finder for the patient portal.

        GET /api/v1/practice/<uid>/portal/available-slots/

        Returns slots on a single date, restricted to operatories flagged
        ``is_web_sched=True``.  The response omits operatory identifiers; the
        patient picks a time, and the booking endpoint chooses a concrete
        chair server-side.
      parameters:
        - in: query
          name: date
          schema:
            type: string
            format: date
          description: Target date (YYYY-MM-DD).
          required: true
        - in: query
          name: duration_minutes
          schema:
            type: integer
          description: Required appointment length in minutes.
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: integer
          description: Optional provider ID filter.
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: >-
            List of available slots on the given date.  Each slot is
            ``{start_time, end_time, provider_name?}``; operatory identifiers
            are intentionally omitted.
        '400':
          description: Missing or malformed query parameters.
        '401':
          description: Authentication required.
        '403':
          description: Not a portal (patient) user.
        '404':
          description: No patient record for this user at the practice.
        '429':
          description: Throttled — portal slot-search rate exceeded.
  /api/v1/practice/{practice_uid}/portal/balance/:
    get:
      operationId: practice_portal_balance_retrieve
      description: |-
        Compact balance summary for the patient portal landing page.

        GET /api/v1/practice/<uid>/portal/balance/

        Returns:

        * ``total_balance`` -- net amount currently owed (positive) or
          held as credit (negative).  Computed from the full ledger.
        * ``current_charges`` -- portion of the balance from charges
          dated within the last ``_PORTAL_OVERDUE_DAYS`` days.
        * ``overdue_charges`` -- portion of the balance from charges
          dated more than ``_PORTAL_OVERDUE_DAYS`` days ago.  Capped
          at ``max(0, total_balance)``.
        * ``last_payment_date`` / ``last_payment_amount`` -- most recent
          patient-paid (non-insurance) payment, or null if none.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortalBalance'
              examples:
                PatientBalanceSummary:
                  value:
                    total_balance: '1035.00'
                    current_charges: '1035.00'
                    overdue_charges: '0.00'
                    last_payment_date: '2026-05-14'
                    last_payment_amount: '150.00'
                  summary: 200 OK -- compact balance for the portal landing page
                  description: >-
                    ``total_balance`` is the net amount owed (positive) or held
                    as credit (negative).  The balance is split into
                    ``current_charges`` (within the last 30 days) and
                    ``overdue_charges`` (older than 30 days, never above
                    ``total_balance``).  ``last_payment_*`` tracks only
                    patient-paid payments -- insurance write-backs are excluded.
          description: ''
        '404':
          description: No patient record found for this portal user.
  /api/v1/practice/{practice_uid}/portal/find-spot/:
    get:
      operationId: practice_portal_find_spot_retrieve
      description: |-
        Multi-day slot finder for the patient portal.

        GET /api/v1/practice/<uid>/portal/find-spot/

        Scans a date range and returns the first matching slots across all
        web-scheduling-eligible operatories.  Search window is capped at 90
        days and result count at 20 — both tighter than the staff endpoint.
      parameters:
        - in: query
          name: duration_minutes
          schema:
            type: integer
          description: Required appointment length in minutes.
          required: true
        - in: query
          name: end_date
          schema:
            type: string
            format: date
          description: >-
            Last date to search (default: start_date + 30 days).  ``end_date -
            start_date`` must be <= 90 days.
        - in: query
          name: max_results
          schema:
            type: integer
          description: Maximum slots to return (default 20, max 20).
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: integer
          description: Optional provider ID filter.
        - in: query
          name: start_date
          schema:
            type: string
            format: date
          description: 'First date to search (default: today).'
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: >-
            Slots matching the constraints.  Each slot is ``{date, start_time,
            end_time, provider_name?}``; operatory identifiers are intentionally
            omitted.
        '400':
          description: >-
            Missing / malformed parameters, ``max_results > 20``, or ``end_date
            - start_date > 90 days``.
        '401':
          description: Authentication required.
        '403':
          description: Not a portal (patient) user.
        '404':
          description: No patient record for this user at the practice.
        '429':
          description: Throttled — portal slot-search rate exceeded.
  /api/v1/practice/{practice_uid}/portal/ledger/:
    get:
      operationId: practice_portal_ledger_retrieve
      description: >-
        Patient's ledger showing charges, payments, adjustments, and running
        balance.


        GET /api/v1/practice/<uid>/portal/ledger/


        Returns a paginated, date-descending list of ledger entries.  The

        running balance is computed across the *full* ledger (oldest-first)

        before paginating so each page's ``balance_after`` is correct

        regardless of which page is requested.


        Only the authenticated patient's own data is returned.
      parameters:
        - in: query
          name: page
          schema:
            type: integer
          description: 1-based page number (default 1).
        - in: query
          name: page_size
          schema:
            type: integer
          description: Rows per page (default 20, max 100).
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: integer
                  next:
                    type: string
                    format: uri
                    nullable: true
                  previous:
                    type: string
                    format: uri
                    nullable: true
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                          format: date
                        type:
                          type: string
                          enum:
                            - charge
                            - payment
                            - insurance_payment
                            - adjustment
                        entry_type:
                          type: string
                          enum:
                            - charge
                            - payment
                            - insurance_payment
                            - adjustment
                        description:
                          type: string
                        amount:
                          type: string
                        balance_after:
                          type: string
                        running_balance:
                          type: string
                  balance:
                    type: string
              examples:
                LedgerWithRunningBalance:
                  value:
                    entries:
                      - date: '2026-03-02'
                        entry_type: charge
                        description: D1110 Prophylaxis - adult
                        amount: '110.00'
                        running_balance: '110.00'
                      - date: '2026-03-02'
                        entry_type: payment
                        description: Balance on previous visit
                        amount: '75.00'
                        running_balance: '35.00'
                      - date: '2026-05-14'
                        entry_type: charge
                        description: D2740 Crown - porcelain/ceramic substrate
                        amount: '1150.00'
                        running_balance: '1185.00'
                      - date: '2026-05-14'
                        entry_type: payment
                        description: Co-pay for cleaning
                        amount: '150.00'
                        running_balance: '1035.00'
                    balance: '1035.00'
                  summary: 200 OK -- chronological entries + net balance
                  description: >-
                    Ledger entries are sorted oldest-first so
                    ``running_balance`` reads like a check register. 
                    ``balance`` at the envelope level is the net amount the
                    patient currently owes (positive means the practice is owed
                    money; negative means the patient has a credit).
          description: >-
            Paginated ledger.  ``results`` carries the page's rows (newest
            first); each row has ``date``, ``type``
            (``charge``/``payment``/``insurance_payment``/``adjustment``),
            ``description``, ``amount``, and ``balance_after``.  ``balance``
            (envelope) is the patient's current net balance across the *full*
            ledger, not just the current page.
        '404':
          description: No patient record found for this portal user.
  /api/v1/practice/{practice_uid}/portal/message/:
    get:
      operationId: practice_portal_message_list
      description: >-
        List or send messages for the authenticated patient.


        Only patient users (is_patient=True) may access this endpoint.

        Messages are scoped to the practice in the URL and the authenticated
        user.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPortalMessageList'
              examples:
                PatientThread(twoMessages):
                  value:
                    count: 123
                    next: http://api.example.org/accounts/?page=4
                    previous: http://api.example.org/accounts/?page=2
                    results:
                      - count: 2
                        next: null
                        previous: null
                        results:
                          - id: dddddddd-dddd-4ddd-8ddd-dddddddddddd
                            patient: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa
                            practice: 99999999-9999-4999-8999-999999999999
                            subject: 'Re: Question about my upcoming cleaning'
                            body: >-
                              Coffee is fine! Please just avoid eating within 30
                              minutes of your appointment.
                            is_from_patient: false
                            is_read: true
                            created: '2026-05-10T16:03:41Z'
                            custom_data: {}
                          - id: cccccccc-cccc-4ccc-8ccc-cccccccccccc
                            patient: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa
                            practice: 99999999-9999-4999-8999-999999999999
                            subject: Question about my upcoming cleaning
                            body: >-
                              Hi! Is it OK to drink coffee the morning of my
                              cleaning appointment?
                            is_from_patient: true
                            is_read: false
                            created: '2026-05-10T14:22:08Z'
                            custom_data: {}
                  summary: Paginated list of portal messages
                  description: >-
                    Typical response for a patient checking their inbox. 
                    Results are ordered newest-first by ``created`` and wrapped
                    in the project-wide paginator envelope (``count``, ``next``,
                    ``previous``, ``results``).
          description: ''
    post:
      operationId: practice_portal_message_create
      description: >-
        List or send messages for the authenticated patient.


        Only patient users (is_patient=True) may access this endpoint.

        Messages are scoped to the practice in the URL and the authenticated
        user.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PortalMessageRequest'
            examples:
              SendAPortalMessage:
                value:
                  subject: Question about my upcoming cleaning
                  body: >-
                    Hi! Is it OK to drink coffee the morning of my cleaning
                    appointment?
                summary: Patient composes a new message to the practice
                description: >-
                  Only ``subject`` and ``body`` are writable -- ``patient``,
                  ``practice`` and ``is_from_patient`` are server-set from the
                  authenticated user and URL scope.
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PortalMessageRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PortalMessageRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortalMessage'
              examples:
                MessageSent:
                  value:
                    id: cccccccc-cccc-4ccc-8ccc-cccccccccccc
                    patient: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa
                    practice: 99999999-9999-4999-8999-999999999999
                    subject: Question about my upcoming cleaning
                    body: >-
                      Hi! Is it OK to drink coffee the morning of my cleaning
                      appointment?
                    is_from_patient: true
                    is_read: false
                    created: '2026-05-10T14:22:08Z'
                    custom_data: {}
                  summary: 201 Created -- message persisted
                  description: >-
                    Full message record with server-assigned id, patient,
                    practice, and timestamp.
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/portal/message/{message_uid}/:
    get:
      operationId: practice_portal_message_retrieve
      description: |-
        Retrieve a single portal message.

        Practice-scoped and patient-scoped.
      parameters:
        - in: path
          name: message_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortalMessage'
              examples:
                PortalMessageDetail:
                  value:
                    id: dddddddd-dddd-4ddd-8ddd-dddddddddddd
                    patient: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa
                    practice: 99999999-9999-4999-8999-999999999999
                    subject: 'Re: Question about my upcoming cleaning'
                    body: >-
                      Coffee is fine! Please just avoid eating within 30 minutes
                      of your appointment.
                    is_from_patient: false
                    is_read: true
                    created: '2026-05-10T16:03:41Z'
                    custom_data: {}
                  summary: Single message
                  description: >-
                    Same shape as the list entries.  Fetching a message marks it
                    viewed for audit purposes.
          description: ''
  /api/v1/practice/{practice_uid}/portal/patient-submissions/:
    get:
      operationId: practice_portal_patient_submissions_list
      description: |-
        List patient submissions for a practice.

        GET /api/v1/practice/<uid>/patient-submissions/

        Defaults to pending submissions. Use ``?status=approved`` or
        ``?status=rejected`` to filter.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Patient Submissions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPatientSubmissionListList'
              examples:
                SubmissionsDashboard:
                  value:
                    count: 123
                    next: http://api.example.org/accounts/?page=4
                    previous: http://api.example.org/accounts/?page=2
                    results:
                      - count: 3
                        next: null
                        previous: null
                        results:
                          - id: eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee
                            practice: 99999999-9999-4999-8999-999999999999
                            invite_token: example-synthetic-token-DO-NOT-USE-0123456789
                            invite_expires_at: '2026-05-20T00:00:00Z'
                            status: pending
                            submitted_at: null
                            reviewed_at: null
                            reviewed_by: null
                            patient: null
                            created: '2026-05-13T12:00:00Z'
                          - id: ffffffff-ffff-4fff-8fff-ffffffffffff
                            practice: 99999999-9999-4999-8999-999999999999
                            invite_token: example-synthetic-token-DO-NOT-USE-0123456789
                            invite_expires_at: '2026-05-20T00:00:00Z'
                            status: pending
                            submitted_at: '2026-05-14T10:15:00Z'
                            reviewed_at: null
                            reviewed_by: null
                            patient: null
                            created: '2026-05-13T12:00:00Z'
                          - id: abababab-abab-4bab-8bab-abababababab
                            practice: 99999999-9999-4999-8999-999999999999
                            invite_token: example-synthetic-token-DO-NOT-USE-0123456789
                            invite_expires_at: '2026-05-20T00:00:00Z'
                            status: approved
                            submitted_at: '2026-05-14T10:15:00Z'
                            reviewed_at: '2026-05-14T11:02:47Z'
                            reviewed_by: bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb
                            patient: 11111111-1111-4111-8111-111111111111
                            created: '2026-05-13T12:00:00Z'
                  summary: Paginated list -- pending + approved
                  description: >-
                    Default ordering is newest-first by ``created``.  Filter
                    with ``?status=pending`` / ``?status=approved`` /
                    ``?status=rejected`` to scope the front-desk review queue.
          description: ''
  /api/v1/practice/{practice_uid}/portal/patient-submissions/{id}/:
    get:
      operationId: practice_portal_patient_submissions_retrieve
      description: |-
        Retrieve a single patient submission with full detail.

        GET /api/v1/practice/<uid>/patient-submissions/<id>/
      parameters:
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Patient Submissions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientSubmissionDetail'
              examples:
                SubmissionDetailWithSubmittedData:
                  value:
                    id: abababab-abab-4bab-8bab-abababababab
                    practice: 99999999-9999-4999-8999-999999999999
                    invite_token: example-synthetic-token-DO-NOT-USE-0123456789
                    invite_expires_at: '2026-05-20T00:00:00Z'
                    status: approved
                    submitted_at: '2026-05-14T10:15:00Z'
                    reviewed_at: '2026-05-14T11:02:47Z'
                    reviewed_by: bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb
                    patient: 11111111-1111-4111-8111-111111111111
                    created: '2026-05-13T12:00:00Z'
                    submitted_data:
                      first_name: John
                      last_name: Doe
                      email: john.doe@example.com
                      date_of_birth: '1990-04-12'
                      gender: M
                      phone_home: '+15555550100'
                      phone_mobile: '+15555550101'
                      phone_work: ''
                      address: 123 Example St
                      address2: ''
                      city: Springfield
                      state: IL
                      zip_code: '62701'
                      insurance:
                        carrier: Synthetic Dental PPO
                        subscriber_id: SYN-000-0001
                        group_number: GRP-9999
                      medical_history:
                        allergies: []
                        medications: []
                        conditions: []
                    rejection_reason: ''
                  summary: Full submission record including the intake payload
                  description: >-
                    Returned to staff reviewing a submission. 
                    ``submitted_data`` is an open JSON object -- shape is
                    defined by the intake form the patient filled out.  No
                    schema validation is enforced here; the approve step copies
                    known fields to the ``Patient`` record.
          description: ''
  /api/v1/practice/{practice_uid}/portal/patient-submissions/{id}/approve/:
    post:
      operationId: practice_portal_patient_submissions_approve_create
      description: |-
        Approve a patient submission — creates a Patient record.

        POST /api/v1/practice/<uid>/patient-submissions/<pk>/approve/

        Extracts demographics from ``submitted_data`` to create the
        ``Patient`` record, links it to the submission, and sets
        ``status=approved``.
      parameters:
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Patient Submissions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientSubmissionDetail'
              examples:
                SubmissionApproved:
                  value:
                    id: abababab-abab-4bab-8bab-abababababab
                    practice: 99999999-9999-4999-8999-999999999999
                    invite_token: example-synthetic-token-DO-NOT-USE-0123456789
                    invite_expires_at: '2026-05-20T00:00:00Z'
                    status: approved
                    submitted_at: '2026-05-14T10:15:00Z'
                    reviewed_at: '2026-05-14T11:02:47Z'
                    reviewed_by: bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb
                    patient: 11111111-1111-4111-8111-111111111111
                    created: '2026-05-13T12:00:00Z'
                    submitted_data:
                      first_name: John
                      last_name: Doe
                      email: john.doe@example.com
                      date_of_birth: '1990-04-12'
                      gender: M
                      phone_home: '+15555550100'
                      phone_mobile: '+15555550101'
                      phone_work: ''
                      address: 123 Example St
                      address2: ''
                      city: Springfield
                      state: IL
                      zip_code: '62701'
                      insurance:
                        carrier: Synthetic Dental PPO
                        subscriber_id: SYN-000-0001
                        group_number: GRP-9999
                      medical_history:
                        allergies: []
                        medications: []
                        conditions: []
                    rejection_reason: ''
                  summary: 200 OK -- Patient record created and linked
                  description: >-
                    The server creates a ``Patient`` record from
                    ``submitted_data`` and sets ``status=approved``,
                    ``reviewed_by`` to the current user, and ``patient`` to the
                    new record's id.
          description: ''
        '400':
          description: Patient has not submitted data yet.
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Submission not found.
        '409':
          description: Submission has already been approved or rejected.
  /api/v1/practice/{practice_uid}/portal/patient-submissions/{id}/reject/:
    post:
      operationId: practice_portal_patient_submissions_reject_create
      description: |-
        Reject a patient submission with a reason.

        POST /api/v1/practice/<uid>/patient-submissions/<pk>/reject/
      parameters:
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Patient Submissions
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatientSubmissionRejectRequest'
            examples:
              RejectWithReason:
                value:
                  rejection_reason: >-
                    Duplicate submission -- patient already has an active
                    record.
                summary: Staff rejects a submission
                description: >-
                  ``rejection_reason`` is required and surfaced to the patient
                  (not stored as PHI).
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatientSubmissionRejectRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatientSubmissionRejectRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientSubmissionDetail'
              examples:
                SubmissionRejected:
                  value:
                    id: abababab-abab-4bab-8bab-abababababab
                    practice: 99999999-9999-4999-8999-999999999999
                    invite_token: example-synthetic-token-DO-NOT-USE-0123456789
                    invite_expires_at: '2026-05-20T00:00:00Z'
                    status: rejected
                    submitted_at: '2026-05-14T10:15:00Z'
                    reviewed_at: '2026-05-14T11:02:47Z'
                    reviewed_by: bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb
                    patient: null
                    created: '2026-05-13T12:00:00Z'
                    submitted_data:
                      first_name: John
                      last_name: Doe
                      email: john.doe@example.com
                      date_of_birth: '1990-04-12'
                      gender: M
                      phone_home: '+15555550100'
                      phone_mobile: '+15555550101'
                      phone_work: ''
                      address: 123 Example St
                      address2: ''
                      city: Springfield
                      state: IL
                      zip_code: '62701'
                      insurance:
                        carrier: Synthetic Dental PPO
                        subscriber_id: SYN-000-0001
                        group_number: GRP-9999
                      medical_history:
                        allergies: []
                        medications: []
                        conditions: []
                    rejection_reason: >-
                      Duplicate submission -- patient already has an active
                      record.
                  summary: 200 OK -- submission marked rejected
                  description: >-
                    Returns the full submission with ``status=rejected`` and the
                    reviewer stamped.
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Submission not found.
        '409':
          description: Submission has already been reviewed.
  /api/v1/practice/{practice_uid}/portal/patient-submissions/invite/:
    post:
      operationId: practice_portal_patient_submissions_invite_create
      description: |-
        Generate a one-time invite link for a prospective patient.

        POST /api/v1/practice/<uid>/patient-submissions/invite/

        Only Office Manager or above may generate invites.  Creates a
        ``PatientSubmission`` with a unique token and 7-day expiry.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Patient Submissions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatientSubmissionInviteResponse'
              examples:
                InviteCreated:
                  value:
                    id: eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee
                    invite_token: example-synthetic-token-DO-NOT-USE-0123456789
                    invite_expires_at: '2026-05-20T00:00:00Z'
                    created: '2026-05-13T12:00:00Z'
                  summary: 201 Created -- one-time invite ready to share
                  description: >-
                    Front-desk staff copies ``invite_token`` into the invite URL
                    (``/api/v1/portal/submission/<token>/``) and emails or texts
                    it to the prospective patient.  Expires after 7 days or on
                    first use, whichever comes first.
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/portal/payment-methods/:
    get:
      operationId: practice_portal_payment_methods_retrieve
      description: |-
        List the patient's saved Stripe payment methods.

        GET /api/v1/practice/<uid>/portal/payment-methods/

        Returns a sanitized shape — only brand / last4 / expiry are exposed
        to the frontend.  When the patient has no Stripe Customer yet (never
        saved a card) we return an empty list rather than 404 so the UI can
        render its "no saved cards" empty state without special-casing.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentMethodListResponse'
          description: ''
        '404':
          description: No patient record for the authenticated portal user.
        '502':
          description: Stripe API error — list could not be fetched.
        '503':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeNotConfigured'
          description: ''
  /api/v1/practice/{practice_uid}/portal/payment-methods/{pm_id}/:
    delete:
      operationId: practice_portal_payment_methods_destroy
      description: |-
        Detach a single saved payment method.

        DELETE /api/v1/practice/<uid>/portal/payment-methods/<pm_id>/

        Verifies the payment method belongs to the authenticated patient's
        Stripe Customer before detaching — without this check, a malicious
        portal user could detach another patient's card by guessing
        ``pm_xxx`` ids.
      parameters:
        - in: path
          name: pm_id
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: Payment method detached.
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Payment method does not belong to this patient.
        '502':
          description: Stripe API error — detach failed.
        '503':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeNotConfigured'
          description: ''
  /api/v1/practice/{practice_uid}/portal/payments/:
    get:
      operationId: practice_portal_payments_list
      description: |-
        List patient-paid payment history for the authenticated patient.

        Insurance-source payments (``pay_type`` classified as
        ``insurance``) are filtered out — patient-portal callers want
        their own out-of-pocket payments, not EOB write-backs.  The
        ledger endpoint surfaces insurance payments separately.

        Paginated via :class:`StandardPageNumberPagination` so a long
        history doesn't ship in a single response.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PortalPaymentRead'
              examples:
                PatientPaymentHistory:
                  value:
                    - - id: cdcdcdcd-cdcd-4dcd-8dcd-cdcdcdcdcdcd
                        pay_date: '2026-05-14'
                        pay_amount: '150.00'
                        pay_type: online_patient_portal
                        pay_note: Co-pay for cleaning
                        created: '2026-05-14T18:02:19Z'
                      - id: dadadada-dada-4ada-8ada-dadadadadada
                        pay_date: '2026-03-02'
                        pay_amount: '75.00'
                        pay_type: online_patient_portal
                        pay_note: Balance on previous visit
                        created: '2026-03-02T15:44:02Z'
                  summary: 200 OK -- newest payment first
                  description: >-
                    The patient sees only their own payments in this practice. 
                    ``pay_type`` is stable (`online_patient_portal` for
                    portal-posted payments); Stripe processing is stubbed today
                    and will land once the BAA is finalised.
          description: ''
    post:
      operationId: practice_portal_payments_create
      description: |-
        Record a patient payment from the portal — charge-then-persist.

        The flow is:

        1. Validate the payload.
        2. If a Stripe ``payment_method`` token was supplied, create a
           confirmed off-session PaymentIntent server-side so the card
           is charged before we persist anything.  A failure here
           returns 402 PAYMENT_REQUIRED with the sanitized Stripe error
           — no ``PaymentRecord`` is written.
        3. Only after a successful charge (or a non-Stripe payload — for
           e.g. the initial post-deploy window when ``STRIPE_SECRET_KEY``
           is unset) do we persist the ``PaymentRecord``.

        503 SERVICE_UNAVAILABLE is returned when a ``payment_method`` is
        supplied but Stripe creds are missing — the frontend treats this
        as "payments not yet enabled in this environment" rather than a
        bug.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PortalPaymentWriteRequest'
            examples:
              PostA$150Co-pay:
                value:
                  amount: '150.00'
                  payment_method: pm_synthetic_placeholder
                  notes: Co-pay for cleaning
                summary: Patient pays a balance from the portal
                description: >-
                  ``amount`` is required and must be positive. 
                  ``payment_method`` is accepted today (Stripe PM token) but
                  deferred until the BAA is finalised -- no card is charged yet,
                  the ``PaymentRecord`` is written directly.
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PortalPaymentWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PortalPaymentWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortalPaymentRead'
              examples:
                PaymentRecorded:
                  value:
                    id: cdcdcdcd-cdcd-4dcd-8dcd-cdcdcdcdcdcd
                    pay_date: '2026-05-14'
                    pay_amount: '150.00'
                    pay_type: online_patient_portal
                    pay_note: Co-pay for cleaning
                    created: '2026-05-14T18:02:19Z'
                  summary: 201 Created -- payment persisted
                  description: >-
                    Returns the freshly created ``PaymentRecord`` in the same
                    shape used by the list endpoint.
          description: ''
        '400':
          description: Validation error (e.g. non-positive amount).
        '402':
          description: Stripe declined the charge — payment_method failed.
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: No patient record found for this portal user.
        '503':
          description: >-
            Stripe integration is wired but credentials are not yet configured
            in this environment (BAA + secret-wiring gates per escape#6).
  /api/v1/practice/{practice_uid}/portal/setup-intents/:
    post:
      operationId: practice_portal_setup_intents_create
      description: |-
        Create a Stripe SetupIntent so the patient can save a card.

        POST /api/v1/practice/<uid>/portal/setup-intents/

        Returns ``client_secret`` for the frontend to call
        ``stripe.confirmSetup`` with.  The Stripe Customer is created on
        demand so the row only exists once the patient actually saves a card.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Portal
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SetupIntentResponse'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: No patient record for the authenticated portal user.
        '502':
          description: Stripe API error — SetupIntent could not be created.
        '503':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeNotConfigured'
          description: ''
  /api/v1/practice/{practice_uid}/prescription/:
    get:
      operationId: practice_prescription_list
      description: |-
        List and create prescriptions.

        ``GET``  /v1/practice/<uid>/prescription/
        ``POST`` /v1/practice/<uid>/prescription/

        Query params
        ------------
        ``patient`` — filter by patient UUID
        ``status``  — filter by status (draft/final/cancelled)
        ``search``  — case-insensitive drug name search
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPrescriptionList'
          description: ''
    post:
      operationId: practice_prescription_create
      description: |-
        List and create prescriptions.

        ``GET``  /v1/practice/<uid>/prescription/
        ``POST`` /v1/practice/<uid>/prescription/

        Query params
        ------------
        ``patient`` — filter by patient UUID
        ``status``  — filter by status (draft/final/cancelled)
        ``search``  — case-insensitive drug name search
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PrescriptionRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PrescriptionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PrescriptionRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Prescription'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/prescription/{prescription_id}/:
    get:
      operationId: practice_prescription_retrieve
      description: |-
        Retrieve or update a prescription (draft only).

        ``GET``   /v1/practice/<uid>/prescription/<id>/
        ``PATCH`` /v1/practice/<uid>/prescription/<id>/

        Only ``draft`` prescriptions may be edited.  Use the ``/finalise/``
        action to advance to ``final``.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: prescription_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Prescription'
          description: ''
    put:
      operationId: practice_prescription_update
      description: |-
        Retrieve or update a prescription (draft only).

        ``GET``   /v1/practice/<uid>/prescription/<id>/
        ``PATCH`` /v1/practice/<uid>/prescription/<id>/

        Only ``draft`` prescriptions may be edited.  Use the ``/finalise/``
        action to advance to ``final``.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: prescription_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PrescriptionRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PrescriptionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PrescriptionRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Prescription'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_prescription_partial_update
      description: |-
        Retrieve or update a prescription (draft only).

        ``GET``   /v1/practice/<uid>/prescription/<id>/
        ``PATCH`` /v1/practice/<uid>/prescription/<id>/

        Only ``draft`` prescriptions may be edited.  Use the ``/finalise/``
        action to advance to ``final``.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: prescription_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPrescriptionRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPrescriptionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPrescriptionRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Prescription'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/prescription/{prescription_id}/cancel/:
    post:
      operationId: practice_prescription_cancel_create
      description: >-
        Cancel a prescription (draft or final).


        ``POST`` /v1/practice/<uid>/prescription/<id>/cancel/


        Any staff member may cancel a draft.  Only ADMIN/PROVIDER may cancel

        a finalised prescription (e.g. if an error was discovered after
        printing).
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: prescription_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/prescription/{prescription_id}/finalise/:
    post:
      operationId: practice_prescription_finalise_create
      description: |-
        Finalise a draft prescription.

        ``POST`` /v1/practice/<uid>/prescription/<id>/finalise/

        Only a user who holds a prescribing-provider role (ADMIN or PROVIDER in
        the practice membership) may finalise.  This represents the provider
        reviewing the draft entered by staff and giving their sign-off.

        For controlled substances, the provider must still physically sign the
        *printed* paper prescription.  This endpoint is NOT an EPCS action and
        does not constitute an electronic DEA signature.

        Returns the updated prescription payload.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: prescription_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/prescription/{prescription_id}/pdf/:
    get:
      operationId: practice_prescription_pdf_retrieve
      description: |-
        Render a prescription as a printable PDF.

        ``GET`` /v1/practice/<uid>/prescription/<id>/pdf/

        Returns ``application/pdf``.  Only ``final`` prescriptions may be
        rendered; draft prescriptions return 400.

        The PDF is formatted as a standard paper Rx form including:
        - Practice name, address, phone
        - Provider name, NPI, DEA number
        - Patient name, date of birth
        - Date of prescription
        - Drug details and directions
        - Blank diagonal line below the drug entry (tamper-evident, required
          for controlled substance paper prescriptions in many states)
        - Provider signature line

        Emits a ``prescription.printed`` audit event so there is a permanent
        record of when the form was accessed for printing.

        **Note**: For paper controlled-substance prescriptions, the provider
        must physically sign the printed form.  This endpoint is not an EPCS
        action and does not constitute a DEA electronic signature.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: prescription_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/pdf:
              schema:
                type: string
                format: binary
          description: ''
  /api/v1/practice/{practice_uid}/prescription/template/:
    get:
      operationId: practice_prescription_template_list
      description: |-
        List and create practice prescription templates.

        ``GET``  /v1/practice/<uid>/prescription/template/
        ``POST`` /v1/practice/<uid>/prescription/template/
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPrescriptionTemplateList'
          description: ''
    post:
      operationId: practice_prescription_template_create
      description: |-
        List and create practice prescription templates.

        ``GET``  /v1/practice/<uid>/prescription/template/
        ``POST`` /v1/practice/<uid>/prescription/template/
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PrescriptionTemplateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PrescriptionTemplateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PrescriptionTemplateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PrescriptionTemplate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/prescription/template/{template_id}/:
    get:
      operationId: practice_prescription_template_retrieve
      description: |-
        Retrieve, update, or delete a prescription template.

        ``GET``    /v1/practice/<uid>/prescription/template/<id>/
        ``PATCH``  /v1/practice/<uid>/prescription/template/<id>/
        ``DELETE`` /v1/practice/<uid>/prescription/template/<id>/
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PrescriptionTemplate'
          description: ''
    put:
      operationId: practice_prescription_template_update
      description: |-
        Retrieve, update, or delete a prescription template.

        ``GET``    /v1/practice/<uid>/prescription/template/<id>/
        ``PATCH``  /v1/practice/<uid>/prescription/template/<id>/
        ``DELETE`` /v1/practice/<uid>/prescription/template/<id>/
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PrescriptionTemplateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PrescriptionTemplateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PrescriptionTemplateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PrescriptionTemplate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_prescription_template_partial_update
      description: |-
        Retrieve, update, or delete a prescription template.

        ``GET``    /v1/practice/<uid>/prescription/template/<id>/
        ``PATCH``  /v1/practice/<uid>/prescription/template/<id>/
        ``DELETE`` /v1/practice/<uid>/prescription/template/<id>/
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPrescriptionTemplateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPrescriptionTemplateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPrescriptionTemplateRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PrescriptionTemplate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_prescription_template_destroy
      description: |-
        Retrieve, update, or delete a prescription template.

        ``GET``    /v1/practice/<uid>/prescription/template/<id>/
        ``PATCH``  /v1/practice/<uid>/prescription/template/<id>/
        ``DELETE`` /v1/practice/<uid>/prescription/template/<id>/
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Prescriptions
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/privacy-config/:
    get:
      operationId: practice_privacy_config_retrieve
      description: >-
        Return the privacy configuration for the practice's state.


        Looks up the ``StatePrivacyConfig`` for the practice's state and returns

        the full config object.  If the state has no privacy law on file,
        returns

        an empty config dict.


        Staff-auth required; practice-scoped via ``PracticeScopedMixin``.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Practices
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatePrivacyConfig'
          description: ''
  /api/v1/practice/{practice_uid}/provider/:
    get:
      operationId: practice_provider_list
      description: >-
        List and create providers for a practice.


        The list endpoint currently returns every provider for the practice
        without

        server-side filtering; clients (e.g. the staff web app) filter in-memory
        by

        ``is_active`` / ``is_hidden`` for dropdown rendering.  If server-side

        filtering becomes a hot path, add explicit ``OpenApiParameter`` entries
        here

        so the schema stays honest.


        Uses :class:`ReferenceDataPagination` (100/2000) instead of the default

        20/100 cap because every staff surface (schedule, checkout, claim,
        chart)

        needs the full provider roster for local filtering.  Historical imports

        can push a DSO's combined roster past the 100-row default.  See api#194.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Providers
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedProviderList'
          description: ''
    post:
      operationId: practice_provider_create
      description: >-
        List and create providers for a practice.


        The list endpoint currently returns every provider for the practice
        without

        server-side filtering; clients (e.g. the staff web app) filter in-memory
        by

        ``is_active`` / ``is_hidden`` for dropdown rendering.  If server-side

        filtering becomes a hot path, add explicit ``OpenApiParameter`` entries
        here

        so the schema stays honest.


        Uses :class:`ReferenceDataPagination` (100/2000) instead of the default

        20/100 cap because every staff surface (schedule, checkout, claim,
        chart)

        needs the full provider roster for local filtering.  Historical imports

        can push a DSO's combined roster past the 100-row default.  See api#194.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Providers
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProviderRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ProviderRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ProviderRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Provider'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_provider_bulk_partial_update
      description: Handle PATCH on the list endpoint for bulk updates.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Providers
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedProviderRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedProviderRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedProviderRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Provider'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/provider/{provider_uid}/:
    get:
      operationId: practice_provider_retrieve
      description: Retrieve or update a single provider.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: provider_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Provider'
          description: ''
    put:
      operationId: practice_provider_update
      description: Retrieve or update a single provider.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: provider_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProviderRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ProviderRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ProviderRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Provider'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_provider_partial_update
      description: Retrieve or update a single provider.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: provider_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedProviderRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedProviderRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedProviderRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Provider'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/provider/operatory/:
    get:
      operationId: practice_provider_operatory_list
      description: >-
        List and create operatories for a practice.


        Returns every operatory for the practice; clients filter client-side for

        schedule rendering (``is_hygiene``, ``is_hidden``, ``is_web_sched``). 
        Add

        ``OpenApiParameter`` entries here if server-side filters are introduced.


        Uses :class:`ReferenceDataPagination` (100/2000) instead of the default

        20/100 cap because the schedule grid needs every operatory (including

        hidden / legacy ones for historical appointment context) in one request.

        See api#194.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Providers
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedOperatoryList'
          description: ''
    post:
      operationId: practice_provider_operatory_create
      description: >-
        List and create operatories for a practice.


        Returns every operatory for the practice; clients filter client-side for

        schedule rendering (``is_hygiene``, ``is_hidden``, ``is_web_sched``). 
        Add

        ``OpenApiParameter`` entries here if server-side filters are introduced.


        Uses :class:`ReferenceDataPagination` (100/2000) instead of the default

        20/100 cap because the schedule grid needs every operatory (including

        hidden / legacy ones for historical appointment context) in one request.

        See api#194.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Providers
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OperatoryRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OperatoryRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/OperatoryRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Operatory'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_provider_operatory_bulk_partial_update
      description: Handle PATCH on the list endpoint for bulk updates.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Providers
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedOperatoryRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedOperatoryRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedOperatoryRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Operatory'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/provider/operatory/{operatory_uid}/:
    get:
      operationId: practice_provider_operatory_retrieve
      description: |-
        Retrieve, update, or delete a single operatory.

        Delete is an admin-rare path — the web app prefers a soft-hide
        (``is_hidden=True`` + ``is_bookable=False``) so historical appointments
        keep their operatory context.  When an admin does choose to delete, we
        let the FK's ``CASCADE`` clean up dependents (schedule templates, legacy
        appointment rows, etc.) and emit an ``operatory.deleted`` audit event.
      parameters:
        - in: path
          name: operatory_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Operatory'
          description: ''
    put:
      operationId: practice_provider_operatory_update
      description: |-
        Retrieve, update, or delete a single operatory.

        Delete is an admin-rare path — the web app prefers a soft-hide
        (``is_hidden=True`` + ``is_bookable=False``) so historical appointments
        keep their operatory context.  When an admin does choose to delete, we
        let the FK's ``CASCADE`` clean up dependents (schedule templates, legacy
        appointment rows, etc.) and emit an ``operatory.deleted`` audit event.
      parameters:
        - in: path
          name: operatory_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OperatoryRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OperatoryRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/OperatoryRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Operatory'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_provider_operatory_partial_update
      description: |-
        Retrieve, update, or delete a single operatory.

        Delete is an admin-rare path — the web app prefers a soft-hide
        (``is_hidden=True`` + ``is_bookable=False``) so historical appointments
        keep their operatory context.  When an admin does choose to delete, we
        let the FK's ``CASCADE`` clean up dependents (schedule templates, legacy
        appointment rows, etc.) and emit an ``operatory.deleted`` audit event.
      parameters:
        - in: path
          name: operatory_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedOperatoryRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedOperatoryRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedOperatoryRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Operatory'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_provider_operatory_destroy
      description: |-
        Retrieve, update, or delete a single operatory.

        Delete is an admin-rare path — the web app prefers a soft-hide
        (``is_hidden=True`` + ``is_bookable=False``) so historical appointments
        keep their operatory context.  When an admin does choose to delete, we
        let the FK's ``CASCADE`` clean up dependents (schedule templates, legacy
        appointment rows, etc.) and emit an ``operatory.deleted`` audit event.
      parameters:
        - in: path
          name: operatory_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/scheduling/appointment/:
    get:
      operationId: practice_scheduling_appointment_list
      description: |-
        List and create appointments. Staff-only.

        List supports filtering by date range (``from`` / ``to``), ``status``,
        ``patient``, and ``provider``.  Results are ordered by ``date_time``
        ascending with a ``pk`` tiebreaker (see :class:`AppointmentPagination`).

        **Status vocabulary** -- the Django state machine uses its own status
        slugs, distinct from any PMS-side ``arrival_status`` value carried by
        sync tools.  The three statuses that matter for the No-show report are:

        - ``broken`` -- patient did not arrive (**no-show**).  The
          ``Status.BROKEN`` choice is labelled ``Broken/No-Show`` in the model.
          This is the value the No-show report filters on.
        - ``cancelled`` -- patient cancelled ahead of time (slot could be
          refilled; does not count as a no-show).
        - ``complete`` -- patient arrived and the visit finished normally.

        For convenience, ``?status=no_show`` is accepted as an alias for
        ``?status=broken`` so the web app can keep semantic URLs.

        **No-show shortcut** -- ``?no_show_between=YYYY-MM-DD,YYYY-MM-DD``
        expands to ``status=no_show`` plus ``from``/``to`` using the supplied
        dates.  Explicit ``from`` / ``to`` / ``status`` query params still win
        if passed alongside, so the shortcut is purely additive.

        **No-show annotations** -- when the resolved status is ``broken`` /
        ``no_show``, each row is additionally annotated with:

        - ``patient_no_show_count`` -- lifetime count of ``broken`` appointments
          for the patient scoped to this practice (ignores other practices).
        - ``patient_phone`` -- patient's best reach-out number
          (``phone_mobile`` preferred, then ``phone_home``, then ``phone_work``).
        - ``patient_outstanding_balance`` -- the patient's current aged
          receivables snapshot (``Patient.balance_current``).
        - ``cancel_reason`` -- the appointment's ``cancellation_reason`` if set.

        These fields are ``null`` on rows that are not no-shows, keeping the
        payload lean for normal schedule queries.
      parameters:
        - in: query
          name: from
          schema:
            type: string
            format: date
          description: >-
            Inclusive lower bound on ``date_time`` (ISO 8601 date,
            ``YYYY-MM-DD``). Interpreted in the practice's local timezone -- a
            ``from=2025-06-01`` filter returns appointments whose local-time
            start is on or after midnight on June 1 in the practice's zone.
          examples:
            MonthStart:
              value: '2025-06-01'
              summary: Month start
        - in: query
          name: no_show_between
          schema:
            type: string
          description: >-
            Shortcut for the No-show report: two ISO dates separated by a comma
            (``YYYY-MM-DD,YYYY-MM-DD``) expands to ``status=no_show`` +
            ``from=<first>`` + ``to=<second>``. Explicit ``from`` / ``to`` /
            ``status`` query params still win if supplied alongside. Example:
            ``no_show_between=2025-06-01,2025-06-30``.
          examples:
            June2025:
              value: 2025-06-01,2025-06-30
              summary: June 2025
        - in: query
          name: page
          schema:
            type: integer
          description: >-
            1-indexed page number. Pagination is keyed by ``date_time`` with a
            ``pk`` tiebreaker for stability.
        - in: query
          name: page_size
          schema:
            type: integer
          description: >-
            Results per page. Default 50, max 100 for regular clients.
            First-party sync tools (escape-eaglesoft, escape-opendental) bypass
            the cap via ``StandardPageNumberPagination`` so they can fetch large
            historical windows.
        - in: query
          name: patient
          schema:
            type: integer
          description: >-
            Filter by patient integer PK. Combine with ``from``/``to`` for a
            patient visit history slice.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: integer
          description: Filter by primary provider integer PK.
        - in: query
          name: status
          schema:
            type: string
            enum:
              - arrived
              - broken
              - cancelled
              - complete
              - no_show
              - note
              - other
              - planned
              - ready_for_doctor
              - ready_for_walkout
              - scheduled
              - seated
              - unscheduled
          description: >-
            Filter by appointment status slug. Status vocabulary used by the
            Django state machine (distinct from any PMS-specific arrival_status
            values on the sync side):


            - ``broken`` -- patient did not arrive for the appointment (a.k.a.
            **no-show**). The model's ``Status.BROKEN`` choice is labelled
            ``Broken/No-Show``; this is the value the No-show report filters on.

            - ``cancelled`` -- patient cancelled the appointment ahead of time.

            - ``scheduled`` / ``arrived`` / ``seated`` / ``ready_for_doctor`` /
            ``ready_for_walkout`` / ``complete`` -- normal patient-flow states.


            ``status=no_show`` is accepted as an alias for ``status=broken`` so
            the web-facing report URLs can stay semantic.
        - in: query
          name: to
          schema:
            type: string
            format: date
          description: >-
            Inclusive upper bound on ``date_time`` (ISO 8601 date,
            ``YYYY-MM-DD``). Interpreted in the practice's local timezone -- the
            full ``to`` day is included (the filter resolves to midnight at the
            start of the *following* day).
          examples:
            MonthEnd:
              value: '2025-06-30'
              summary: Month end
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedAppointmentList'
              examples:
                PaginatedAppointments(weekView):
                  value:
                    count: 123
                    next: http://api.example.org/accounts/?page=4
                    previous: http://api.example.org/accounts/?page=2
                    results:
                      - count: 3
                        next: null
                        previous: null
                        results:
                          - id: 55555555-5555-4555-8555-555555555555
                            patient: 11111111-1111-4111-8111-111111111111
                            provider: 22222222-2222-4222-8222-222222222222
                            provider_hygienist: 33333333-3333-4333-8333-333333333333
                            operatory: 44444444-4444-4444-8444-444444444444
                            treatment_plan: null
                            date_time: '2026-05-14T09:00:00-07:00'
                            duration_minutes: 60
                            status: scheduled
                            confirmed_status: not_called
                            appointment_type: Crown Prep
                            is_hygiene: false
                            is_new_patient: false
                            procedure_description: D2740 Crown - porcelain/ceramic substrate
                            note: Patient prefers morning appointments.
                            cancellation_reason: ''
                            reschedule_requested: false
                            arrived_at: null
                            seated_at: null
                            ready_for_doctor_at: null
                            ready_for_walkout_at: null
                            dismissed_at: null
                            patient_name: Example, Jane
                            provider_name: Synthetic, Dr. Dana
                            provider_hygienist_name: Synthetic, Hollis
                            scheduled_procedures:
                              - id: 1001
                                procedure_code: null
                                proc_code: D2740
                                description: Crown - porcelain/ceramic substrate
                                fee: '1150.00'
                                tooth_num: '14'
                                surface: ''
                                status: planned
                                custom_data: {}
                            custom_data: {}
                          - id: 66666666-6666-4666-8666-666666666666
                            patient: 11111111-1111-4111-8111-111111111111
                            provider: 22222222-2222-4222-8222-222222222222
                            provider_hygienist: 33333333-3333-4333-8333-333333333333
                            operatory: 44444444-4444-4444-8444-444444444444
                            treatment_plan: null
                            date_time: '2026-05-14T10:30:00-07:00'
                            duration_minutes: 45
                            status: scheduled
                            confirmed_status: confirmed
                            appointment_type: Recall / Prophy
                            is_hygiene: true
                            is_new_patient: false
                            procedure_description: D1110 Prophylaxis - adult
                            note: Patient prefers morning appointments.
                            cancellation_reason: ''
                            reschedule_requested: false
                            arrived_at: null
                            seated_at: null
                            ready_for_doctor_at: null
                            ready_for_walkout_at: null
                            dismissed_at: null
                            patient_name: Example, John
                            provider_name: Synthetic, Dr. Dana
                            provider_hygienist_name: Synthetic, Hollis
                            scheduled_procedures:
                              - id: 1002
                                procedure_code: null
                                proc_code: D1110
                                description: Prophylaxis - adult
                                fee: '110.00'
                                tooth_num: ''
                                surface: ''
                                status: planned
                                custom_data: {}
                            custom_data: {}
                          - id: 77777777-7777-4777-8777-777777777777
                            patient: 11111111-1111-4111-8111-111111111111
                            provider: 22222222-2222-4222-8222-222222222222
                            provider_hygienist: 33333333-3333-4333-8333-333333333333
                            operatory: 44444444-4444-4444-8444-444444444444
                            treatment_plan: null
                            date_time: '2026-05-15T14:00:00-07:00'
                            duration_minutes: 90
                            status: scheduled
                            confirmed_status: not_called
                            appointment_type: New Patient Exam
                            is_hygiene: false
                            is_new_patient: true
                            procedure_description: D0150 Comprehensive oral evaluation
                            note: Patient prefers morning appointments.
                            cancellation_reason: ''
                            reschedule_requested: false
                            arrived_at: null
                            seated_at: null
                            ready_for_doctor_at: null
                            ready_for_walkout_at: null
                            dismissed_at: null
                            patient_name: Example, Alex
                            provider_name: Synthetic, Dr. Dana
                            provider_hygienist_name: Synthetic, Hollis
                            scheduled_procedures:
                              - id: 1003
                                procedure_code: null
                                proc_code: D0150
                                description: Comprehensive oral evaluation
                                fee: '95.00'
                                tooth_num: ''
                                surface: ''
                                status: planned
                                custom_data: {}
                            custom_data: {}
                  summary: Three appointments across two days
                  description: >-
                    A typical staff-app response when fetching a week of
                    appointments. Results are ordered by ``date_time``; the
                    response envelope is the project-wide paginator shape
                    (``count``, ``next``, ``previous``, ``results``).
          description: ''
    post:
      operationId: practice_scheduling_appointment_create
      description: |-
        List and create appointments. Staff-only.

        List supports filtering by date range (``from`` / ``to``), ``status``,
        ``patient``, and ``provider``.  Results are ordered by ``date_time``
        ascending with a ``pk`` tiebreaker (see :class:`AppointmentPagination`).

        **Status vocabulary** -- the Django state machine uses its own status
        slugs, distinct from any PMS-side ``arrival_status`` value carried by
        sync tools.  The three statuses that matter for the No-show report are:

        - ``broken`` -- patient did not arrive (**no-show**).  The
          ``Status.BROKEN`` choice is labelled ``Broken/No-Show`` in the model.
          This is the value the No-show report filters on.
        - ``cancelled`` -- patient cancelled ahead of time (slot could be
          refilled; does not count as a no-show).
        - ``complete`` -- patient arrived and the visit finished normally.

        For convenience, ``?status=no_show`` is accepted as an alias for
        ``?status=broken`` so the web app can keep semantic URLs.

        **No-show shortcut** -- ``?no_show_between=YYYY-MM-DD,YYYY-MM-DD``
        expands to ``status=no_show`` plus ``from``/``to`` using the supplied
        dates.  Explicit ``from`` / ``to`` / ``status`` query params still win
        if passed alongside, so the shortcut is purely additive.

        **No-show annotations** -- when the resolved status is ``broken`` /
        ``no_show``, each row is additionally annotated with:

        - ``patient_no_show_count`` -- lifetime count of ``broken`` appointments
          for the patient scoped to this practice (ignores other practices).
        - ``patient_phone`` -- patient's best reach-out number
          (``phone_mobile`` preferred, then ``phone_home``, then ``phone_work``).
        - ``patient_outstanding_balance`` -- the patient's current aged
          receivables snapshot (``Patient.balance_current``).
        - ``cancel_reason`` -- the appointment's ``cancellation_reason`` if set.

        These fields are ``null`` on rows that are not no-shows, keeping the
        payload lean for normal schedule queries.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppointmentCreateRequest'
            examples:
              BookA60-minuteCrownPrep:
                value:
                  patient: 11111111-1111-4111-8111-111111111111
                  provider: 22222222-2222-4222-8222-222222222222
                  operatory: 44444444-4444-4444-8444-444444444444
                  date_time: '2026-05-14T09:00:00-07:00'
                  duration_minutes: 60
                  appointment_type: Crown Prep
                  is_hygiene: false
                  procedure_description: D2740 Crown - porcelain/ceramic substrate
                  note: Patient prefers morning appointments.
                  scheduled_procedures:
                    - proc_code: D2740
                      description: Crown - porcelain/ceramic substrate
                      fee: '1150.00'
                      tooth_num: '14'
                summary: Schedule a crown-prep appointment for a new patient
                description: >-
                  Minimal payload to create a scheduled appointment.
                  ``date_time`` MUST be an ISO 8601 string with a timezone
                  offset — the API rejects naive datetimes. 
                  ``duration_minutes`` is required for conflict-checking.  Set
                  ``allow_overbooking=true`` to skip the double-book guard (used
                  by import/sync tools).
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AppointmentCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/AppointmentCreateRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppointmentCreate'
              examples:
                AppointmentCreated:
                  value:
                    id: 55555555-5555-4555-8555-555555555555
                    patient: 11111111-1111-4111-8111-111111111111
                    provider: 22222222-2222-4222-8222-222222222222
                    provider_hygienist: 33333333-3333-4333-8333-333333333333
                    operatory: 44444444-4444-4444-8444-444444444444
                    treatment_plan: null
                    date_time: '2026-05-14T09:00:00-07:00'
                    duration_minutes: 60
                    status: scheduled
                    confirmed_status: not_called
                    appointment_type: Crown Prep
                    is_hygiene: false
                    is_new_patient: false
                    procedure_description: D2740 Crown - porcelain/ceramic substrate
                    note: Patient prefers morning appointments.
                    cancellation_reason: ''
                    reschedule_requested: false
                    arrived_at: null
                    seated_at: null
                    ready_for_doctor_at: null
                    ready_for_walkout_at: null
                    dismissed_at: null
                    patient_name: Example, Jane
                    provider_name: Synthetic, Dr. Dana
                    provider_hygienist_name: Synthetic, Hollis
                    scheduled_procedures:
                      - id: 1001
                        procedure_code: null
                        proc_code: D2740
                        description: Crown - porcelain/ceramic substrate
                        fee: '1150.00'
                        tooth_num: '14'
                        surface: ''
                        status: planned
                        custom_data: {}
                    custom_data: {}
                  summary: 201 Created response
                  description: >-
                    Response returned after successfully creating the
                    appointment above.
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_scheduling_appointment_bulk_partial_update
      description: Handle PATCH on the list endpoint for bulk updates.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedAppointmentRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedAppointmentRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedAppointmentRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Appointment'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/scheduling/appointment-status/:
    get:
      operationId: practice_scheduling_appointment_status_list
      description: Read-only list of appointment status metadata.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AppointmentStatus'
          description: ''
  /api/v1/practice/{practice_uid}/scheduling/appointment-status/{slug}/:
    get:
      operationId: practice_scheduling_appointment_status_retrieve
      description: Read-only list of appointment status metadata.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: slug
          schema:
            type: string
          description: A unique value identifying this appointment status.
          required: true
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppointmentStatus'
          description: ''
  /api/v1/practice/{practice_uid}/scheduling/appointment/{appointment_uid}/:
    get:
      operationId: practice_scheduling_appointment_retrieve
      description: >-
        Retrieve or update a single appointment. Status changes must go through
        /transition/.
      parameters:
        - in: path
          name: appointment_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Appointment'
          description: ''
    put:
      operationId: practice_scheduling_appointment_update
      description: >-
        Retrieve or update a single appointment. Status changes must go through
        /transition/.
      parameters:
        - in: path
          name: appointment_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppointmentUpdateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AppointmentUpdateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/AppointmentUpdateRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppointmentUpdate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_scheduling_appointment_partial_update
      description: >-
        Retrieve or update a single appointment. Status changes must go through
        /transition/.
      parameters:
        - in: path
          name: appointment_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedAppointmentUpdateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedAppointmentUpdateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedAppointmentUpdateRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppointmentUpdate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/scheduling/appointment/{appointment_uid}/confirm/:
    post:
      operationId: practice_scheduling_appointment_confirm_create
      description: |-
        Update appointment confirmation status (orthogonal to lifecycle status).

        POST {"confirmed_status": "Confirmed"}
      parameters:
        - in: path
          name: appointment_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppointmentConfirmRequest'
            examples:
              MarkAsConfirmed:
                value:
                  confirmed_status: confirmed
                summary: Set confirmation status after patient confirms
                description: >-
                  Confirmation is orthogonal to lifecycle status — an
                  appointment can be ``scheduled`` and ``confirmed`` at the same
                  time.  Valid values: ``not_called``, ``left_message``,
                  ``confirmed``, ``econfirmed``.
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AppointmentConfirmRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/AppointmentConfirmRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Appointment'
              examples:
                AppointmentAfterConfirmation:
                  value:
                    id: 55555555-5555-4555-8555-555555555555
                    patient: 11111111-1111-4111-8111-111111111111
                    provider: 22222222-2222-4222-8222-222222222222
                    provider_hygienist: 33333333-3333-4333-8333-333333333333
                    operatory: 44444444-4444-4444-8444-444444444444
                    treatment_plan: null
                    date_time: '2026-05-14T09:00:00-07:00'
                    duration_minutes: 60
                    status: scheduled
                    confirmed_status: confirmed
                    appointment_type: Crown Prep
                    is_hygiene: false
                    is_new_patient: false
                    procedure_description: D2740 Crown - porcelain/ceramic substrate
                    note: Patient prefers morning appointments.
                    cancellation_reason: ''
                    reschedule_requested: false
                    arrived_at: null
                    seated_at: null
                    ready_for_doctor_at: null
                    ready_for_walkout_at: null
                    dismissed_at: null
                    patient_name: Example, Jane
                    provider_name: Synthetic, Dr. Dana
                    provider_hygienist_name: Synthetic, Hollis
                    scheduled_procedures:
                      - id: 1001
                        procedure_code: null
                        proc_code: D2740
                        description: Crown - porcelain/ceramic substrate
                        fee: '1150.00'
                        tooth_num: '14'
                        surface: ''
                        status: planned
                        custom_data: {}
                    custom_data: {}
                  summary: 200 OK — full appointment with new confirmed_status
                  description: Response mirrors the appointment-detail shape.
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Appointment not found.
  /api/v1/practice/{practice_uid}/scheduling/appointment/{appointment_uid}/history/:
    get:
      operationId: practice_scheduling_appointment_history_list
      description: Return the status transition history for an appointment.
      parameters:
        - in: path
          name: appointment_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AppointmentStatusLog'
              examples:
                StatusTransitionHistory:
                  value:
                    - - id: 1
                        previous_status: scheduled
                        new_status: arrived
                        changed_by: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa
                        changed_by_name: Dana Synthetic
                        changed_at: '2026-05-14T16:00:12Z'
                        note: Checked in at front desk.
                        custom_data: {}
                      - id: 2
                        previous_status: arrived
                        new_status: seated
                        changed_by: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa
                        changed_by_name: Dana Synthetic
                        changed_at: '2026-05-14T16:05:48Z'
                        note: ''
                        custom_data: {}
                  summary: Chronological list of status changes
                  description: >-
                    One entry per transition, oldest first.  ``changed_by`` is
                    null for automated transitions (e.g. system auto-marking
                    broken appointments overnight).
          description: ''
        '404':
          description: Appointment not found.
  /api/v1/practice/{practice_uid}/scheduling/appointment/{appointment_uid}/route-slip/:
    get:
      operationId: practice_scheduling_appointment_route_slip_retrieve
      description: >-
        Aggregate endpoint for the front-desk route-slip print template
        (api#239).


        ``GET /v1/practice/<uid>/scheduling/appointment/<appt_uid>/route-slip/``


        Returns every field the print template renders -- appointment,

        patient, procedures, primary insurance, provider, and operatory --

        in a single JSON response.  Designed for under ~50ms per call so

        check-in at the front desk stays snappy; the N+1 alternative (fetch

        the appointment, then patient, then insurance, then procedures) was

        measured as a noticeable stall while the patient stood at the

        counter.
      parameters:
        - in: path
          name: appointment_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RouteSlipResponse'
          description: ''
  /api/v1/practice/{practice_uid}/scheduling/appointment/{appointment_uid}/transition/:
    post:
      operationId: practice_scheduling_appointment_transition_create
      description: >-
        Transition an appointment to a new status with validation and audit
        logging.


        POST {"status": "complete", "note": "optional reason"}
      parameters:
        - in: path
          name: appointment_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppointmentTransitionRequest'
            examples:
              PatientArrived(scheduled->Arrived):
                value:
                  status: arrived
                  note: Checked in at front desk.
                summary: Mark appointment as arrived at the front desk
                description: >-
                  Call when the patient physically arrives.  The API stamps
                  ``arrived_at`` with the current server time (UTC).
              PatientSeated(arrived->Seated):
                value:
                  status: seated
                  note: ''
                summary: Patient taken to the operatory
                description: Stamps ``seated_at``. The patient is now in the chair.
              ReadyForDoctor(seated->ReadyForDoctor):
                value:
                  status: ready_for_doctor
                  note: Hygiene prophy complete, ready for exam.
                summary: Hygienist signals the doctor is needed for exam
                description: >-
                  Used mid-hygiene visit when the hygienist is ready for the
                  doctor to come in for the exam.  Stamps
                  ``ready_for_doctor_at``.
              ReadyForWalkout(seated->ReadyForWalkout):
                value:
                  status: ready_for_walkout
                  note: ''
                summary: Clinical work done, patient ready to check out
                description: 'Stamps ``ready_for_walkout_at``.  Next stop: ``complete``.'
              Complete(readyForWalkout->Complete):
                value:
                  status: complete
                  note: Walked out, next recall scheduled.
                summary: Patient checked out — appointment finished
                description: >-
                  Final state for a successful visit.  Stamps ``dismissed_at``. 
                  Completed appointments cannot be transitioned further.
              Broken/No-show(scheduled->Broken):
                value:
                  status: broken
                  note: No call, no show.
                summary: Patient did not show up
                description: >-
                  Use ``broken`` for no-shows and same-day cancellations where
                  the slot is now wasted.  Use ``cancelled`` for advance-notice
                  cancellations where the slot can be re-booked.
              Cancelled(scheduled->Cancelled):
                value:
                  status: cancelled
                  note: Patient called to cancel — work conflict.
                summary: Patient cancelled with advance notice
                description: >-
                  Use when the patient cancelled far enough out to re-book the
                  slot.
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AppointmentTransitionRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/AppointmentTransitionRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Appointment'
              examples:
                TransitionSucceeded:
                  value:
                    id: 55555555-5555-4555-8555-555555555555
                    patient: 11111111-1111-4111-8111-111111111111
                    provider: 22222222-2222-4222-8222-222222222222
                    provider_hygienist: 33333333-3333-4333-8333-333333333333
                    operatory: 44444444-4444-4444-8444-444444444444
                    treatment_plan: null
                    date_time: '2026-05-14T09:00:00-07:00'
                    duration_minutes: 60
                    status: arrived
                    confirmed_status: not_called
                    appointment_type: Crown Prep
                    is_hygiene: false
                    is_new_patient: false
                    procedure_description: D2740 Crown - porcelain/ceramic substrate
                    note: Patient prefers morning appointments.
                    cancellation_reason: ''
                    reschedule_requested: false
                    arrived_at: '2026-05-14T16:00:12Z'
                    seated_at: null
                    ready_for_doctor_at: null
                    ready_for_walkout_at: null
                    dismissed_at: null
                    patient_name: Example, Jane
                    provider_name: Synthetic, Dr. Dana
                    provider_hygienist_name: Synthetic, Hollis
                    scheduled_procedures:
                      - id: 1001
                        procedure_code: null
                        proc_code: D2740
                        description: Crown - porcelain/ceramic substrate
                        fee: '1150.00'
                        tooth_num: '14'
                        surface: ''
                        status: planned
                        custom_data: {}
                    custom_data: {}
                  summary: 200 OK — appointment after transition
                  description: >-
                    Returns the full appointment resource with the new
                    ``status`` and patient-flow timestamps.
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Appointment not found.
        '409':
          description: Illegal transition from current status.
  /api/v1/practice/{practice_uid}/scheduling/available-slots/:
    get:
      operationId: practice_scheduling_available_slots_retrieve
      description: |-
        Find available appointment slots for a given date.

        Query params:
            date: Date in YYYY-MM-DD format (required).
            duration_minutes: Appointment length in minutes (required).
            provider: Optional provider ID.
            operatory: Optional operatory ID.
            interval: Slot interval in minutes (default 15).

        Returns only slot times — does not leak why other times are unavailable.
      parameters:
        - in: query
          name: date
          schema:
            type: string
            format: date
          description: Date (YYYY-MM-DD)
          required: true
        - in: query
          name: duration_minutes
          schema:
            type: integer
          description: Required duration
          required: true
        - in: query
          name: interval
          schema:
            type: integer
          description: Slot interval in minutes (default 15)
        - in: query
          name: operatory
          schema:
            type: array
            items:
              type: integer
          description: >-
            Filter by operatory. Repeat the param for a union:
            ?operatory=3&operatory=4.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: array
            items:
              type: integer
          description: >-
            Filter by provider. Repeat the param for a union:
            ?provider=1&provider=2.
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/practice/{practice_uid}/scheduling/day-sheet/:
    get:
      operationId: practice_scheduling_day_sheet_retrieve
      description: >-
        Return the complete day-sheet for operatory-based schedule rendering.


        Single request returning operatories, schedule blocks, and appointments.


        Each entry in ``operatories[]`` carries an ``in_chair_patient`` field

        (GH-713) — ``None`` when the chair is empty, otherwise the

        authoritatively-resolved currently-seated patient:

            {
                "patient_id": "<uuid>",
                "patient_name": "Last, First",
                "appointment_id": "<uuid> | null",
                "since": "<ISO 8601 UTC>",
            }

        Resolution rules (see
        :func:`apps.scheduling.services._resolve_in_chair_patients`):


        * Status is one of ``seated`` / ``ready_for_doctor`` /
        ``ready_for_walkout``
          (the patient is in the chair across all three; ``arrived`` is lobby-only,
          ``complete`` sets ``dismissed_at``).
        * ``dismissed_at IS NULL`` as a safety net for imported data.

        * On multiple "in chair" rows per operatory (back-to-back seatings
        without
          a dismiss — see GH-713 issue body), the most-recently-seated row wins.
        * Walk-ins: not exposed today (no walk-in-patient surface exists in the
          API yet — ``OperatoryPresence`` tracks staff occupancy, not patients).
          The ``appointment_id`` field is nullable so the shape is forward-compatible.

        Query params:
            date: Date in YYYY-MM-DD format (required).
            include_hidden: "true" to include cancelled/unscheduled/planned appointments (default false).
      parameters:
        - in: query
          name: date
          schema:
            type: string
            format: date
          description: Day sheet date (YYYY-MM-DD)
          required: true
        - in: query
          name: include_hidden
          schema:
            type: boolean
          description: Include hidden appointments
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DaySheetResponse'
          description: ''
  /api/v1/practice/{practice_uid}/scheduling/find-spot/:
    get:
      operationId: practice_scheduling_find_spot_retrieve
      description: |-
        Multi-day slot search with patient scheduling constraints.

        All constraint logic runs server-side. Clients send one request with
        constraints and get back matching slots across multiple days.

        Query params:
            duration_minutes: Required appointment length in minutes.
            start_date: First date to search (default: today). YYYY-MM-DD.
            end_date: Last date to search (default: start_date + 30 days). YYYY-MM-DD.
            days_of_week: Legacy shorthand — comma-separated weekday ints (0=Mon..6=Sun).
            earliest_time: Legacy shorthand — earliest acceptable start time (HH:MM).
            latest_time: Legacy shorthand — latest acceptable start time (HH:MM).
            availability_windows: Repeatable ``<days>:<start>-<end>`` windows. Union semantics.
                Mutually exclusive with the legacy ``days_of_week`` / ``earliest_time`` /
                ``latest_time`` params — supply one form or the other.
            provider: Optional provider ID(s). Repeatable for union.
            operatory: Optional operatory ID(s). Repeatable for union.
            max_results: Max slots to return (default 20, max 50).
      parameters:
        - in: query
          name: availability_windows
          schema:
            type: array
            items:
              type: string
          description: >-
            Disjoint availability windows. Each value is
            ``<days>:<start>-<end>`` where ``<days>`` is a comma-separated list
            of weekday ints (0=Mon..6=Sun) and each time bound is ``HH:MM`` or
            blank. Repeat the param for a union. Example:
            ``?availability_windows=4:15:00-&availability_windows=0,2,4:-10:00``
            means "Thursdays 3 PM onwards OR Mon/Wed/Fri before 10 AM". Mutually
            exclusive with the legacy ``days_of_week`` / ``earliest_time`` /
            ``latest_time`` trio.
        - in: query
          name: days_of_week
          schema:
            type: string
          description: >-
            Legacy single-window shorthand: comma-separated weekday ints
            (0=Mon..6=Sun). Mutually exclusive with ``availability_windows``.
        - in: query
          name: duration_minutes
          schema:
            type: integer
          description: Required duration
          required: true
        - in: query
          name: earliest_time
          schema:
            type: string
          description: >-
            Legacy single-window shorthand: earliest start time (HH:MM).
            Mutually exclusive with ``availability_windows``.
        - in: query
          name: end_date
          schema:
            type: string
            format: date
          description: Last date to search (max 90 days)
        - in: query
          name: latest_time
          schema:
            type: string
          description: >-
            Legacy single-window shorthand: latest start time (HH:MM). Mutually
            exclusive with ``availability_windows``.
        - in: query
          name: max_results
          schema:
            type: integer
          description: Max results (default 20, max 50)
        - in: query
          name: operatory
          schema:
            type: array
            items:
              type: integer
          description: >-
            Filter by operatory. Repeat the param for a union:
            ?operatory=3&operatory=4.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: array
            items:
              type: integer
          description: >-
            Filter by provider. Repeat the param for a union:
            ?provider=1&provider=2.
        - in: query
          name: start_date
          schema:
            type: string
            format: date
          description: 'First date to search (default: today)'
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/practice/{practice_uid}/scheduling/month-summary/:
    get:
      operationId: practice_scheduling_month_summary_retrieve
      description: >-
        Per-day appointment aggregate for the month-view calendar grid.


        ``GET
        /v1/practice/<uid>/scheduling/month-summary/?from=YYYY-MM-DD&to=YYYY-MM-DD``


        Returns a sparse map of local-date strings to

        ``{appointment_count, production}`` in a single round-trip, replacing

        the auto-paginated ``/scheduling/appointment/`` fetch loop used by the

        web month-view (``useMonthData``).


        Business rules:


        * Days are bucketed in the **practice's timezone**, never server UTC
          nor the browser's timezone.  An appointment at 11 PM local on
          ``2026-04-01`` stays on that date regardless of the server's wall
          clock.
        * ``appointment_count`` and ``production`` both **exclude**
          appointments whose status is ``cancelled`` or ``broken``, matching
          the client-side filter the web app used to apply.
        * ``production`` sums ``AppointmentProcedure.fee`` for every
          scheduled procedure attached to an included appointment on the day.
        * The response is sparse: only days with at least one *included*
          appointment appear.  Callers treat absent keys as empty days.
        * ``from`` / ``to`` are both **inclusive**.  No server-side range cap
          is imposed (the caller bounds it to ~6 weeks for the month grid);
          the endpoint is a single GROUP BY query covered by
          ``(practice_id, date_time)`` so a reasonable window stays well
          under 50 ms even at 2000 appointments.
      parameters:
        - in: query
          name: from
          schema:
            type: string
            format: date
          description: Inclusive start date (YYYY-MM-DD), interpreted in practice timezone.
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: to
          schema:
            type: string
            format: date
          description: Inclusive end date (YYYY-MM-DD), interpreted in practice timezone.
          required: true
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MonthSummaryResponse'
          description: ''
  /api/v1/practice/{practice_uid}/scheduling/open-close/:
    get:
      operationId: practice_scheduling_open_close_retrieve
      description: |-
        Return the practice's open/close times for a date from appointment data.

        ``GET /v1/practice/<uid>/scheduling/open-close/?date=YYYY-MM-DD``

        "Open" is the earliest qualifying appointment's start; "close" is the
        latest qualifying appointment's end (``start + duration``).  The
        endpoint is intentionally shaped for third-party integrations that
        simply need "when does the office open today?" without having to
        iterate the full schedule.

        Business rules:

        * ``date`` is interpreted in the practice's timezone.  An appointment
          whose local start time falls on that date counts, regardless of
          server UTC or the caller's wall clock.
        * Appointments with status ``cancelled`` or ``broken`` (the model's
          combined Broken / No-Show choice) are **excluded** -- a cancelled
          8 AM slot does not make the office "open at 8:00".
        * ``open`` / ``close`` are returned as ISO-8601 datetimes with the
          practice's TZ offset baked in (e.g. ``2026-04-22T08:15:00-04:00``).
          The offset reflects the practice's timezone *on that calendar
          date*, so DST transitions produce the right offset on each side
          of the jump.  Consumers that want bare wall-clock time can slice
          the string; consumers that need an unambiguous instant already
          have one.
        * No qualifying appointments yields ``{"date": ..., "open": null,
          "close": null}`` (HTTP 200) -- no data is a valid answer, not a 404.

        Auth: any authenticated user with practice access.  OAuth tokens must
        carry the ``scheduling:read`` scope; JWT / session callers pass
        through :class:`SchedulingScope` unchanged.
      parameters:
        - in: query
          name: date
          schema:
            type: string
            format: date
          description: >-
            Target date (``YYYY-MM-DD``) interpreted in the practice's local
            timezone.  An appointment that starts at 8:15 AM local on that date
            yields an ``open`` timestamp anchored to 08:15 on that date with the
            practice's TZ offset.
          required: true
          examples:
            ExampleDate:
              value: '2026-04-22'
              summary: Example date
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OpenCloseResponse'
              examples:
                PopulatedDay:
                  value:
                    date: '2026-04-22'
                    open: '2026-04-22T08:15:00-04:00'
                    close: '2026-04-22T17:30:00-04:00'
                  summary: Populated day
                NoAppointments:
                  value:
                    date: '2026-04-22'
                    open: null
                    close: null
                  summary: No appointments
          description: ''
  /api/v1/practice/{practice_uid}/scheduling/operatory/{operatory_id}/walkin/:
    post:
      operationId: practice_scheduling_operatory_walkin_create
      description: >-
        Seat a patient as a walk-in in the given operatory (GH-715).


        POST
        ``/api/v1/practice/<practice_uid>/scheduling/operatory/<operatory_id>/walkin/``


        Body: ``{"patient": "<patient_uid>"}``


        Creates an :class:`OperatoryWalkIn` row with ``seated_at=now()`` and

        surfaces the patient in the chair-grid's ``in_chair_patient`` field

        on the next day-sheet fetch (with ``appointment_id: null``).


        Refuses with HTTP 400 when an active walk-in already covers the same

        operatory -- a chair holds at most one patient at a time.  Callers

        must dismiss the existing walk-in (or transition a scheduled

        appointment to ``seated``, which auto-dismisses the walk-in) before

        seating a second one.
      parameters:
        - in: path
          name: operatory_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OperatoryWalkInCreateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OperatoryWalkInCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/OperatoryWalkInCreateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperatoryWalkIn'
          description: ''
        '400':
          description: Operatory already has an active walk-in, or patient not in practice.
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Operatory not found.
  /api/v1/practice/{practice_uid}/scheduling/operatory/{operatory_id}/walkin/dismiss/:
    post:
      operationId: practice_scheduling_operatory_walkin_dismiss_create
      description: >-
        Dismiss the active walk-in for the given operatory (GH-715).


        POST
        ``/api/v1/practice/<practice_uid>/scheduling/operatory/<operatory_id>/walkin/dismiss/``


        Sets ``dismissed_at = now()`` on the active :class:`OperatoryWalkIn`

        row for the operatory, returning the closed row.  Returns 404 when

        no active walk-in exists for that chair (also covers cross-tenant:

        operatories from another practice are filtered out before lookup).
      parameters:
        - in: path
          name: operatory_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperatoryWalkIn'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Operatory not found, or no active walk-in for it.
  /api/v1/practice/{practice_uid}/scheduling/recall/:
    get:
      operationId: practice_scheduling_recall_list
      description: >-
        List and create recall / recare records for a practice.


        Supports filtering by ``patient``, ``recall_type``, ``is_disabled``,

        due-date range (``due_before`` / ``due_after``), and ``unscheduled``
        status.
      parameters:
        - in: query
          name: due_after
          schema:
            type: string
            format: date
          description: >-
            Inclusive lower bound on ``date_due`` (ISO 8601 date,
            ``YYYY-MM-DD``).
          examples:
            DueFromToday:
              value: '2025-06-01'
              summary: Due from today
        - in: query
          name: due_before
          schema:
            type: string
            format: date
          description: >-
            Inclusive upper bound on ``date_due`` (ISO 8601 date,
            ``YYYY-MM-DD``).
          examples:
            DueThisQuarter:
              value: '2025-09-30'
              summary: Due this quarter
        - in: query
          name: is_disabled
          schema:
            type: string
            enum:
              - 'false'
              - 'true'
          description: Filter by disabled status. Accepts ``true`` or ``false``.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: query
          name: patient
          schema:
            type: integer
          description: Filter by patient integer PK.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: recall_type
          schema:
            type: string
          description: >-
            Filter by recall type slug. The field is free-form
            (``CharField(max_length=100)``) to accommodate PMS-specific
            categories during migrations; common values include ``prophy``,
            ``perio``, and ``exam``.
          examples:
            Prophy:
              value: prophy
            PerioMaintenance:
              value: perio
              summary: Perio maintenance
            PeriodicExam:
              value: exam
              summary: Periodic exam
        - in: query
          name: unscheduled
          schema:
            type: string
            enum:
              - 'false'
              - 'true'
          description: >-
            Filter to recalls whose ``date_scheduled`` is null (``true``) or
            non-null (``false``). Drives the operations-team 'unscheduled
            recalls' worklist.
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedRecallList'
          description: ''
    post:
      operationId: practice_scheduling_recall_create
      description: >-
        List and create recall / recare records for a practice.


        Supports filtering by ``patient``, ``recall_type``, ``is_disabled``,

        due-date range (``due_before`` / ``due_after``), and ``unscheduled``
        status.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecallWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/RecallWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/RecallWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecallWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_scheduling_recall_bulk_partial_update
      description: Handle PATCH on the list endpoint for bulk updates.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedRecallRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedRecallRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedRecallRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Recall'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/scheduling/recall/{recall_uid}/:
    get:
      operationId: practice_scheduling_recall_retrieve
      description: Retrieve, update, or delete a single recall record.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: recall_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Recall'
          description: ''
    put:
      operationId: practice_scheduling_recall_update
      description: Retrieve, update, or delete a single recall record.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: recall_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecallWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/RecallWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/RecallWriteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecallWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_scheduling_recall_partial_update
      description: Retrieve, update, or delete a single recall record.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: recall_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedRecallWriteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedRecallWriteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedRecallWriteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecallWrite'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_scheduling_recall_destroy
      description: Retrieve, update, or delete a single recall record.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: recall_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/scheduling/recall/types/:
    get:
      operationId: practice_scheduling_recall_types_retrieve
      description: >-
        Reference-data endpoint that returns every distinct ``recall_type``
        string currently in use on the practice's :class:`Recall` rows. The web
        Recall Due report uses this to populate its filter dropdown without
        having to harvest types from the visible page of results (which hides
        rare types the user has not scrolled to). Recall type is currently a
        free-form ``CharField`` on :class:`apps.scheduling.models.Recall`, so
        the source of truth is the existing rows -- there is no separate
        reference table.
      summary: List distinct recall types in use at a practice.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecallTypeListResponse'
          description: ''
  /api/v1/practice/{practice_uid}/scheduling/reports/cancellations/:
    get:
      operationId: practice_scheduling_reports_cancellations_retrieve
      description: |-
        Cancellation report with rebook annotations (api#155).

        Returns every appointment whose status is ``cancelled`` and whose
        latest "to cancelled" status-log entry falls inside the requested
        window.  Each row is annotated with rebook status and lead time so
        the front desk can tell at a glance whether the patient got back on
        the books and how much notice they gave.

        Query params:
            cancelled_between  ``YYYY-MM-DD,YYYY-MM-DD`` in practice TZ
                               (preferred).  When present, overrides ``from``
                               / ``to`` below.
            from, to           Inclusive dates in practice TZ.  Used when
                               ``cancelled_between`` is not supplied.

        Either the shortcut or explicit ``from``/``to`` must be provided.
      parameters:
        - in: query
          name: cancelled_between
          schema:
            type: string
          description: >-
            Two ISO dates separated by a comma (``YYYY-MM-DD,YYYY-MM-DD``)
            interpreted in the practice's local timezone.  Filters on the
            cancellation timestamp (latest status-log entry transitioning the
            row to ``cancelled``).  Either this parameter, or explicit
            ``from``/``to`` below, is required.
        - in: query
          name: from
          schema:
            type: string
            format: date
          description: >-
            Inclusive start date (YYYY-MM-DD, practice TZ).  Ignored when
            ``cancelled_between`` is passed.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: to
          schema:
            type: string
            format: date
          description: >-
            Inclusive end date (YYYY-MM-DD, practice TZ).  Ignored when
            ``cancelled_between`` is passed.
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CancellationReportResponse'
          description: ''
  /api/v1/practice/{practice_uid}/scheduling/reports/provider-utilization/:
    get:
      operationId: practice_scheduling_reports_provider_utilization_retrieve
      description: >-
        Provider Utilization -- scheduled patient time / available working time.


        **Denominator rule (Aaron, api#156):** blocked time (lunch, admin
        blocks,

        time-off overrides flagged non-patient) is subtracted from the

        denominator.  A provider with 8h template time and a 1h lunch blockout

        shows a denominator of 7h, not 8h.  Including blocked time would drag

        utilization down for reasons that are not actionable by the front desk.


        Cancelled, broken (no-show), and unscheduled appointments do **not**

        count toward ``scheduled_hours`` -- they're treated as open time since

        the slot was released.


        Query params:
            from          Required.  Inclusive start date (YYYY-MM-DD, practice TZ).
            to            Required.  Inclusive end date (YYYY-MM-DD, practice TZ).
            granularity   Optional.  ``day`` | ``week`` | ``month``.
            provider      Optional.  Provider UID; repeat for multi-select.
      parameters:
        - in: query
          name: from
          schema:
            type: string
            format: date
          description: >-
            Inclusive start date in the practice's local timezone (ISO 8601
            ``YYYY-MM-DD``).
          required: true
        - in: query
          name: granularity
          schema:
            type: string
            enum:
              - day
              - month
              - week
          description: >-
            Bucket size: ``day``, ``week``, or ``month``.  Defaults to ``day``
            when the window is 14 days or less, ``week`` otherwise.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: array
            items:
              type: string
          description: >-
            Restrict the report to specific providers.  Repeat the parameter for
            a multi-select filter, e.g.
            ``?provider=<uuid-a>&provider=<uuid-b>``.  Omit to include every
            active, non-hidden provider in the practice.
        - in: query
          name: to
          schema:
            type: string
            format: date
          description: >-
            Inclusive end date in the practice's local timezone (ISO 8601
            ``YYYY-MM-DD``).  The window must not exceed 12 months.
          required: true
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderUtilizationResponse'
          description: ''
  /api/v1/practice/{practice_uid}/scheduling/resolved-schedule/:
    get:
      operationId: practice_scheduling_resolved_schedule_retrieve
      description: |-
        Return the effective schedule for a date or date range.

        Query params:
            date: Single date (YYYY-MM-DD). Returns blocks for that date.
            date_from / date_to: Date range. Returns blocks keyed by date.
            provider: Optional provider ID to filter to a single provider.
      parameters:
        - in: query
          name: date
          schema:
            type: string
            format: date
          description: Single date (YYYY-MM-DD)
        - in: query
          name: date_from
          schema:
            type: string
            format: date
          description: Range start (YYYY-MM-DD)
        - in: query
          name: date_to
          schema:
            type: string
            format: date
          description: Range end (YYYY-MM-DD)
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: integer
          description: Filter by provider
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResolvedScheduleResponse'
          description: ''
  /api/v1/practice/{practice_uid}/scheduling/schedule-override/:
    get:
      operationId: practice_scheduling_schedule_override_list
      description: >-
        List and create date-specific schedule overrides.


        Supports filtering by ``provider``, ``date_from``/``date_to`` range, and
        ``schedule_type``.
      parameters:
        - in: query
          name: date_from
          schema:
            type: string
            format: date
          description: >-
            Inclusive lower bound on ``schedule_date`` (ISO 8601 date,
            ``YYYY-MM-DD``).
          examples:
            TodayForward:
              value: '2025-06-01'
              summary: Today forward
        - in: query
          name: date_to
          schema:
            type: string
            format: date
          description: >-
            Inclusive upper bound on ``schedule_date`` (ISO 8601 date,
            ``YYYY-MM-DD``).
          examples:
            QuarterEnd:
              value: '2025-09-30'
              summary: Quarter end
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: integer
          description: >-
            Filter to overrides owned by this provider (integer PK). Overrides
            with a null provider are practice-wide entries (holidays, closure
            days).
        - in: query
          name: schedule_type
          schema:
            type: string
            enum:
              - blockout
              - office_hours
              - provider
          description: >-
            Filter by override kind. ``office_hours`` = one-off practice
            open/closed window, ``provider`` = one-off provider availability
            change, ``blockout`` = date-specific unavailable window.
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedScheduleOverrideList'
          description: ''
    post:
      operationId: practice_scheduling_schedule_override_create
      description: >-
        List and create date-specific schedule overrides.


        Supports filtering by ``provider``, ``date_from``/``date_to`` range, and
        ``schedule_type``.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScheduleOverrideRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ScheduleOverrideRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ScheduleOverrideRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduleOverride'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/scheduling/schedule-override/{override_uid}/:
    get:
      operationId: practice_scheduling_schedule_override_retrieve
      description: Retrieve, update, or delete a schedule override.
      parameters:
        - in: path
          name: override_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduleOverride'
          description: ''
    put:
      operationId: practice_scheduling_schedule_override_update
      description: Retrieve, update, or delete a schedule override.
      parameters:
        - in: path
          name: override_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScheduleOverrideRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ScheduleOverrideRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ScheduleOverrideRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduleOverride'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_scheduling_schedule_override_partial_update
      description: Retrieve, update, or delete a schedule override.
      parameters:
        - in: path
          name: override_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedScheduleOverrideRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedScheduleOverrideRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedScheduleOverrideRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduleOverride'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_scheduling_schedule_override_destroy
      description: Retrieve, update, or delete a schedule override.
      parameters:
        - in: path
          name: override_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/scheduling/schedule-template/:
    get:
      operationId: practice_scheduling_schedule_template_list
      description: >-
        List and create recurring schedule templates.


        Supports filtering by ``provider``, ``day_of_week``, and
        ``schedule_type``.
      parameters:
        - in: query
          name: day_of_week
          schema:
            type: integer
            enum:
              - 0
              - 1
              - 2
              - 3
              - 4
              - 5
              - 6
          description: >-
            Filter by day of week. Integer 0-6 where ``0=Sunday`` and
            ``6=Saturday`` (Django ``Weekday`` convention).
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: provider
          schema:
            type: integer
          description: >-
            Filter to templates owned by this provider (integer PK). Templates
            with a null provider are practice-wide entries (e.g. office hours
            when ``schedule_type=office_hours``) and will not match a numeric
            provider filter.
        - in: query
          name: schedule_type
          schema:
            type: string
            enum:
              - blockout
              - office_hours
              - provider
          description: >-
            Filter by template kind. ``office_hours`` = practice-wide open
            hours, ``provider`` = per-provider availability, ``blockout`` =
            recurring unavailable window (lunch, meetings).
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedScheduleTemplateList'
          description: ''
    post:
      operationId: practice_scheduling_schedule_template_create
      description: >-
        List and create recurring schedule templates.


        Supports filtering by ``provider``, ``day_of_week``, and
        ``schedule_type``.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScheduleTemplateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ScheduleTemplateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ScheduleTemplateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduleTemplate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/scheduling/schedule-template/{template_uid}/:
    get:
      operationId: practice_scheduling_schedule_template_retrieve
      description: Retrieve, update, or delete a schedule template.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduleTemplate'
          description: ''
    put:
      operationId: practice_scheduling_schedule_template_update
      description: Retrieve, update, or delete a schedule template.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScheduleTemplateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ScheduleTemplateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ScheduleTemplateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduleTemplate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_scheduling_schedule_template_partial_update
      description: Retrieve, update, or delete a schedule template.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedScheduleTemplateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedScheduleTemplateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedScheduleTemplateRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduleTemplate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    delete:
      operationId: practice_scheduling_schedule_template_destroy
      description: Retrieve, update, or delete a schedule template.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: template_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/scheduling/stats/:
    get:
      operationId: practice_scheduling_stats_retrieve
      description: Quick scheduling stats.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduleStatsResponse'
          description: ''
  /api/v1/practice/{practice_uid}/scheduling/validate-appointment/:
    post:
      operationId: practice_scheduling_validate_appointment_create
      description: |-
        Validate a proposed appointment against scheduling constraints.

        POST {
            "operatory": 1,
            "date_time": "2026-03-19T09:00:00",
            "duration_minutes": 60,
            "provider": 1,                   // optional
            "exclude_appointment": 42         // optional, for rescheduling
        }

        Returns {"issues": [...], "valid": true/false}
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Scheduling
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateAppointmentRequestRequest'
            examples:
              ValidateAProposedSlot:
                value:
                  operatory: 44444444-4444-4444-8444-444444444444
                  date_time: '2026-05-14T09:00:00-07:00'
                  duration_minutes: 60
                  provider: 22222222-2222-4222-8222-222222222222
                  exclude_appointment: 55555555-5555-4555-8555-555555555555
                summary: Dry-run conflict check before creating an appointment
                description: >-
                  Returns scheduling issues without persisting anything.  Useful
                  for drag-to-reschedule UX where the client wants to warn
                  before the drop is committed.  ``exclude_appointment`` ignores
                  conflicts with the appointment being rescheduled.
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ValidateAppointmentRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ValidateAppointmentRequestRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateAppointmentResponse'
              examples:
                SlotIsAvailable:
                  value:
                    valid: true
                    issues: []
                  summary: valid=true with no issues
                  description: Safe to book — no conflicts, no warnings.
                SlotHasAConflict:
                  value:
                    valid: false
                    issues:
                      - level: error
                        code: operatory_conflict
                        message: >-
                          Operatory is already booked 2026-05-14T09:00 —
                          2026-05-14T10:00.
                  summary: valid=false with blocking error
                  description: >-
                    ``valid`` is false when any issue has ``level=='error'``. 
                    Clients typically either block the save or prompt the user
                    to confirm an override (which sends
                    ``allow_overbooking=true`` on the actual create).
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/share/:
    get:
      operationId: practice_share_list
      description: |-
        Create / list shares scoped to the URL's practice.

        * ``GET`` -- paginated list for the My Shares UI.  Supports
          ``?status=``, ``?patient=``, ``?created_from=`` / ``?created_to=``,
          ``?recipient_kind=`` (repeatable, ORs within the param), and
          ``?mine_only=true`` filters.  Ordered by ``-created`` (newest first).
        * ``POST`` -- mint a new share.  The response is the ONLY place the
          caller ever sees the plaintext token / phrase / URL / QR.  The
          model persists only hashes.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedShareListItemList'
          description: ''
    post:
      operationId: practice_share_create
      description: |-
        Create / list shares scoped to the URL's practice.

        * ``GET`` -- paginated list for the My Shares UI.  Supports
          ``?status=``, ``?patient=``, ``?created_from=`` / ``?created_to=``,
          ``?recipient_kind=`` (repeatable, ORs within the param), and
          ``?mine_only=true`` filters.  Ordered by ``-created`` (newest first).
        * ``POST`` -- mint a new share.  The response is the ONLY place the
          caller ever sees the plaintext token / phrase / URL / QR.  The
          model persists only hashes.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ShareCreateInputRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ShareCreateInputRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ShareCreateInputRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShareCreateInput'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/share/{share_uid}/:
    get:
      operationId: practice_share_retrieve
      description: |-
        Detail view for the My Shares audit side panel.

        Reads are read-only; mutations go through the revoke endpoint.  The
        response echoes resolved :class:`ShareItem` rows alongside the share
        fields and the embedded ``events`` timeline (api#240) so the UI can
        render the audit trail without a second fetch.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: share_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShareDetail'
          description: ''
  /api/v1/practice/{practice_uid}/share/{share_uid}/revoke/:
    post:
      operationId: practice_share_revoke_create
      description: |-
        Revoke a share -- stop-future-access, not recall.

        Idempotent: revoking an already-revoked share returns 200 with the
        (unchanged) share detail so the client can retry safely.  Emits a
        ``share.revoked`` audit event on every call, so auditors can see
        repeated revocation attempts (which should never happen in practice
        but which we log regardless).
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: share_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/signatures/:
    get:
      operationId: practice_signatures_list
      description: >-
        Returns the practice's signature rows in ``-signed_at`` order.  Compact
        list shape -- the signed image URL is omitted to keep payloads small;
        open the detail view to fetch it.
      summary: List signature evidence rows for the practice.
      parameters:
        - in: query
          name: attached_content_type
          schema:
            type: string
          description: >-
            Filter to signatures attached to a given content type, as
            "app_label.model" (e.g. "portal.formsubmission").
        - in: query
          name: object_id
          schema:
            type: string
          description: >-
            Filter to signatures attached to a specific object id. Typically
            combined with ``attached_content_type``.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: query
          name: patient
          schema:
            type: string
            format: uuid
          description: Filter to signatures by a specific patient.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: signer_type
          schema:
            type: string
          description: 'Filter by signer type: ``patient`` / ``staff``.'
      tags:
        - Signatures
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedSignatureListList'
          description: ''
    post:
      operationId: practice_signatures_create
      description: >-
        Accepts a rendered signature PNG plus signer metadata and stores the row
        as immutable evidence.  Body is ``multipart/form-data`` with the PNG
        attached as the ``image`` field; the remaining fields ride alongside as
        standard form fields.  Server-managed fields (``practice``,
        ``signed_at``, ``ip_address``, ``user_agent``) are populated server-side
        and any client-supplied values are ignored.  Returns the detail shape on
        201 so the caller can render or download the just-captured signature
        without an extra GET.
      summary: Capture a digital signature (staff-side, multipart upload).
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Signatures
      requestBody:
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/SignatureCreateRequest'
          application/json:
            schema:
              $ref: '#/components/schemas/SignatureCreateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignatureDetail'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/signatures/{signature_uid}/:
    get:
      operationId: practice_signatures_retrieve
      description: >-
        Returns the full row, including a short-lived signed URL for the
        captured image and the consent-text snapshot.  Every read fires a
        ``signature.viewed`` audit event so the access trail is complete --
        signature images are legal evidence and access must be traced.
      summary: Read a single signature evidence row (with signed image URL).
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: signature_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Signatures
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignatureDetail'
          description: ''
        '404':
          description: Not found, or not in the requested practice.
  /api/v1/practice/{practice_uid}/signatures/{signature_uid}/preview-pdf/:
    get:
      operationId: practice_signatures_preview_pdf_retrieve
      description: >-
        Returns a single-page ``application/pdf`` containing the captured
        signature image, the typed signer name, the signed-at timestamp, and the
        verbatim consent-text snapshot.  Useful for letting a practice user
        eyeball a captured signature before binding it to a final document, and
        as an appendix the form-generation pipeline can attach to generated
        paperwork.


        Emits a ``signature.viewed`` audit event because the rendered PDF
        contains the same evidence the detail GET exposes -- the access trail
        must remain complete.
      summary: Render a standalone PDF preview of a signature row.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: signature_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Signatures
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/pdf:
              schema:
                type: string
                format: binary
          description: Signature-block PDF for the requested row.
        '404':
          description: Not found, or not in the requested practice.
  /api/v1/practice/{practice_uid}/sms/consents/:
    get:
      operationId: practice_sms_consents_list
      description: |-
        List SMS consent rows for a single patient (api#637).

        Returns an array of zero or one row given the unique-together
        ``(practice, patient)`` constraint.  The list shape mirrors what
        web#485 already calls -- the staff UI degrades to "no row, default
        opted_in" when the response is empty.

        The ``patient`` query parameter is required.  A practice-wide list
        surface for a future compliance dashboard would be a separate view
        (the issue's optional ``?state=opted_out`` filter); we deliberately
        don't conflate the two so the OpenAPI tag stays narrow.
      parameters:
        - in: query
          name: patient
          schema:
            type: string
            format: uuid
          description: >-
            Filter to the single (practice, patient) consent row. Required --
            the per-practice consent list is reserved for a future
            compliance-dashboard surface; today the patient-scoped lookup is the
            only supported shape.
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SMSConsent'
          description: ''
  /api/v1/practice/{practice_uid}/sms/messages/:
    get:
      operationId: practice_sms_messages_list
      description: List and create SMS messages for the practice.
      parameters:
        - in: query
          name: direction
          schema:
            type: string
            enum:
              - inbound
              - outbound
          description: Filter to inbound or outbound messages.
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: query
          name: patient
          schema:
            type: string
            format: uuid
          description: Filter to messages associated with this patient.
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: status
          schema:
            type: string
            enum:
              - delivered
              - failed
              - queued
              - received
              - sending
              - sent
          description: Filter by per-message delivery state.
      tags:
        - Communications
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedSMSMessageList'
          description: ''
    post:
      operationId: practice_sms_messages_create
      description: List and create SMS messages for the practice.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SMSMessageCreateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/SMSMessageCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/SMSMessageCreateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SMSMessageCreate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/sms/messages/{message_id}/:
    get:
      operationId: practice_sms_messages_retrieve
      description: |-
        Retrieve a single SMS row.

        Read-only -- there's no legitimate reason to PATCH a message row
        in place.  Status changes flow through the (future) Sinch DLR
        webhook handler, not the user-facing API.
      parameters:
        - in: path
          name: message_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SMSMessage'
          description: ''
  /api/v1/practice/{practice_uid}/sms/numbers/:
    get:
      operationId: practice_sms_numbers_list
      description: |-
        List SMS numbers assigned to the practice (history + active).

        Read-only -- provisioning happens via an internal support tool /
        admin action today (see the issue body's "out of scope" + the
        api#622 follow-up for an admin endpoint).  The active number is
        distinguishable by ``deactivated_at IS NULL`` on the row.
      summary: List the practice's provisioned SMS numbers
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PracticeSMSNumber'
          description: ''
  /api/v1/practice/{practice_uid}/sms/read-receipts/:
    get:
      operationId: practice_sms_read_receipts_retrieve
      description: |-
        List + upsert read-receipts for the calling staff user (api#643).

        GET
        ---
        Returns this user's read-receipts at this practice, one row per
        patient.  The response is shaped to be cheap for the inbox to
        consume: ``[{patient_id, last_read_at}]``.  ``?user=me`` is the
        only supported filter today; the parameter is reserved so a future
        cross-user surface can share the same URL.

        POST
        ----
        Upserts the row for ``(this practice, body.patient_id, this user)``.
        Idempotent and monotonic -- a write whose ``last_read_at`` is
        earlier than the current row's value is silently accepted as a
        no-op.  This makes the endpoint safe for late-arriving events
        (slow mobile reconnecting after a laptop has already advanced the
        high-water mark).

        Permissions: any practice member.  Read-receipts aren't HIPAA-
        significant access events (the actual PHI-access event is the
        existing ``sms.viewed`` from api#636), so no audit emission here.
      summary: List the calling user's SMS read receipts for this practice
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: user
          schema:
            type: string
          description: >-
            Sentinel filter -- only ``me`` is supported.  The list is always
            scoped to the calling user; the parameter exists so the URL contract
            reads naturally and so future cross-user surfaces (e.g. a supervisor
            dashboard) can extend the same path.
      tags:
        - Communications
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SMSReadReceipt'
          description: ''
    post:
      operationId: practice_sms_read_receipts_create
      description: |-
        List + upsert read-receipts for the calling staff user (api#643).

        GET
        ---
        Returns this user's read-receipts at this practice, one row per
        patient.  The response is shaped to be cheap for the inbox to
        consume: ``[{patient_id, last_read_at}]``.  ``?user=me`` is the
        only supported filter today; the parameter is reserved so a future
        cross-user surface can share the same URL.

        POST
        ----
        Upserts the row for ``(this practice, body.patient_id, this user)``.
        Idempotent and monotonic -- a write whose ``last_read_at`` is
        earlier than the current row's value is silently accepted as a
        no-op.  This makes the endpoint safe for late-arriving events
        (slow mobile reconnecting after a laptop has already advanced the
        high-water mark).

        Permissions: any practice member.  Read-receipts aren't HIPAA-
        significant access events (the actual PHI-access event is the
        existing ``sms.viewed`` from api#636), so no audit emission here.
      summary: Upsert this user's read-receipt for one patient thread
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Communications
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SMSReadReceiptUpsertRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/SMSReadReceiptUpsertRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/SMSReadReceiptUpsertRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SMSReadReceipt'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/staff/invitations/:
    get:
      operationId: practice_staff_invitations_list
      description: >-
        List or create practice invitations.


        Returns every ``PracticeInvitation`` attached to the practice -- the
        staff

        UI filters by ``status`` (PENDING / ACCEPTED / REVOKED / EXPIRED)
        client-

        side.  ``pagination_class = None`` keeps the response a flat array.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Staff
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PracticeInvitationList'
          description: ''
    post:
      operationId: practice_staff_invitations_create
      description: >-
        List or create practice invitations.


        Returns every ``PracticeInvitation`` attached to the practice -- the
        staff

        UI filters by ``status`` (PENDING / ACCEPTED / REVOKED / EXPIRED)
        client-

        side.  ``pagination_class = None`` keeps the response a flat array.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Staff
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PracticeInvitationCreateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PracticeInvitationCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PracticeInvitationCreateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PracticeInvitationCreate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/staff/invitations/{invitation_uid}/revoke/:
    post:
      operationId: practice_staff_invitations_revoke_create
      description: Revoke a pending practice invitation.
      parameters:
        - in: path
          name: invitation_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Staff
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/staff/members/:
    get:
      operationId: practice_staff_members_list
      description: >-
        List everyone with access to a practice.


        Returns the union of:


        * every :class:`PracticeMembership` for the practice (direct members,
          active and deactivated alike), serialized with ``via_practice=True``;
        * every :class:`ClientMembership` of the practice's parent client
          (organization-level admins -- OWNER / BILLING / TECHNICAL), serialized
          with ``via_client=True`` so the UI can tag the row with an
          "Inherited from organization" badge.

        The two tables don't share a common id space, so the union happens at
        the

        serializer-output level rather than as a SQL ``UNION``.  The response
        stays

        a flat JSON array (``pagination_class = None``) so the staff UI can
        render

        the members table in one pass and the web ``usePracticeMembers`` hook
        can

        keep typing it as ``T[]``.


        **Dedup (api#570):** when a user holds BOTH a ``PracticeMembership`` for

        this practice AND a ``ClientMembership`` for its parent client, only one

        row is emitted.  The direct ``PracticeMembership`` row wins (its per-

        practice ``role`` is concrete and primary) and ``via_client=True`` is
        set

        on that row so the UI can still surface the "also has client-level
        access"

        signal.  This prevents the duplicate row Aaron screenshotted in

        escapedental/escape#169.


        Practice members are emitted first (sorted by display name), client-only

        members second (also sorted by display name).  Display name is the
        user's

        full name with the email as a fallback, so ordering stays deterministic

        even before the user has filled in their profile.


        Read access is open to any authenticated staff user with effective
        access

        to the practice -- including users whose only membership is at the
        client

        level (api#557).  Mutating endpoints continue to gate on practice admin

        roles via :class:`PracticeMemberDetailView`.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Staff
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PracticeMember'
          description: ''
  /api/v1/practice/{practice_uid}/staff/members/{member_uid}/:
    get:
      operationId: practice_staff_members_retrieve
      description: Retrieve or update a single practice membership.
      parameters:
        - in: path
          name: member_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PracticeMember'
          description: ''
    put:
      operationId: practice_staff_members_update
      description: Retrieve or update a single practice membership.
      parameters:
        - in: path
          name: member_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PracticeMemberUpdateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PracticeMemberUpdateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PracticeMemberUpdateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PracticeMemberUpdate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
    patch:
      operationId: practice_staff_members_partial_update
      description: Retrieve or update a single practice membership.
      parameters:
        - in: path
          name: member_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPracticeMemberUpdateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPracticeMemberUpdateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPracticeMemberUpdateRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PracticeMemberUpdate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/sync/export/:
    get:
      operationId: practice_sync_export_retrieve
      description: Stream all practice data as a JSON download.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: >-
            Streaming JSON dump of all practice data. Body shape is documented
            separately; treat as opaque attachment.
  /api/v1/practice/{practice_uid}/sync/export/counts/:
    get:
      operationId: practice_sync_export_counts_retrieve
      description: Return entity counts for a practice (lightweight preview).
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PracticeExportCountsResponse'
          description: ''
  /api/v1/practice/{practice_uid}/sync/import/{entity_type}/:
    post:
      operationId: practice_sync_import_create
      description: |-
        Receive a batch of records for a given entity type.

        Records are bulk-created with ``ignore_conflicts=True`` so re-running
        a migration is safe.
      parameters:
        - in: path
          name: entity_type
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportBatchRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ImportBatchRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ImportBatchRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportBatchResponse'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetailResponse'
          description: ''
        '409':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetailResponse'
          description: ''
  /api/v1/practice/{practice_uid}/sync/import/session/:
    get:
      operationId: practice_sync_import_session_list
      description: |-
        List import sessions (GET) or create a new one (POST).

        GET is open to any authenticated caller — listing one's own import
        sessions is non-sensitive metadata.  POST is restricted to sync tools
        via the ``sync:patients`` gate (first-party bypass keeps the migration
        tools working; third-party apps cannot request ``sync:*`` scopes).
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ImportSessionListItem'
          description: ''
    post:
      operationId: practice_sync_import_session_create
      description: |-
        List import sessions (GET) or create a new one (POST).

        GET is open to any authenticated caller — listing one's own import
        sessions is non-sensitive metadata.  POST is restricted to sync tools
        via the ``sync:patients`` gate (first-party bypass keeps the migration
        tools working; third-party apps cannot request ``sync:*`` scopes).
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportSessionCreateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ImportSessionCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ImportSessionCreateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSessionCreateResponse'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/sync/import/session/{session_id}/:
    get:
      operationId: practice_sync_import_session_retrieve
      description: >-
        Retrieve session detail with conversion progress (GET) or delete
        (DELETE).
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: session_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSessionDetailResponse'
          description: ''
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetailResponse'
          description: ''
    delete:
      operationId: practice_sync_import_session_destroy
      description: >-
        Retrieve session detail with conversion progress (GET) or delete
        (DELETE).
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: session_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: Session deleted.
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetailResponse'
          description: ''
        '409':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetailResponse'
          description: ''
  /api/v1/practice/{practice_uid}/sync/import/session/{session_id}/accept/:
    post:
      operationId: practice_sync_import_session_accept_create
      description: >-
        Accept conversion results (even with errors) and mark session as
        converted.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: session_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversionAcceptResponse'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetailResponse'
          description: ''
        '409':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetailResponse'
          description: ''
  /api/v1/practice/{practice_uid}/sync/import/session/{session_id}/complete/:
    post:
      operationId: practice_sync_import_session_complete_create
      description: Mark an import session as completed.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: session_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportSessionCompleteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ImportSessionCompleteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ImportSessionCompleteRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSessionCompleteResponse'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetailResponse'
          description: ''
  /api/v1/practice/{practice_uid}/sync/import/session/{session_id}/convert/:
    post:
      operationId: practice_sync_import_session_convert_create
      description: Trigger conversion of a completed import session to live data.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: session_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '202':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSessionConvertResponse'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetailResponse'
          description: ''
        '409':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetailResponse'
          description: ''
  /api/v1/practice/{practice_uid}/sync/import/session/{session_id}/error-report/:
    get:
      operationId: practice_sync_import_session_error_report_retrieve
      description: Download a conversion error report as JSON or CSV.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: session_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversionErrorReportResponse'
          description: ''
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetailResponse'
          description: ''
  /api/v1/practice/{practice_uid}/sync/import/session/{session_id}/rollback/:
    post:
      operationId: practice_sync_import_session_rollback_create
      description: >-
        Delete all normalized data for this session and reset to uploaded
        status.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: session_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversionRollbackResponse'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetailResponse'
          description: ''
        '409':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetailResponse'
          description: ''
  /api/v1/practice/{practice_uid}/sync/import/session/{session_id}/schema/:
    post:
      operationId: practice_sync_import_session_schema_create
      description: Upload a source PMS schema dump for an import session.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: session_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportSchemaRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ImportSchemaRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ImportSchemaRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSchemaUploadResponse'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetailResponse'
          description: ''
  /api/v1/practice/{practice_uid}/sync/ping/:
    get:
      operationId: practice_sync_ping_retrieve
      description: Simple authentication check.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PingResponse'
          description: ''
  /api/v1/practice/{practice_uid}/sync/status/:
    get:
      operationId: practice_sync_status_list
      description: List all sync status records for a practice.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SyncStatusResponse'
          description: ''
  /api/v1/practice/{practice_uid}/sync/status/{entity_type}/:
    get:
      operationId: practice_sync_status_retrieve
      description: Get or update a sync status record for a specific entity type.
      parameters:
        - in: path
          name: entity_type
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SyncStatusResponse'
          description: ''
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DetailResponse'
          description: ''
    post:
      operationId: practice_sync_status_create
      description: Get or update a sync status record for a specific entity type.
      parameters:
        - in: path
          name: entity_type
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Import
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SyncStatusUpdateRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/SyncStatusUpdateRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/SyncStatusUpdateRequestRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SyncStatusResponse'
          description: ''
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SyncStatusResponse'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/tablets/:
    get:
      operationId: practice_tablets_list
      description: |-
        Return every Tablet (revoked included) at the calling practice.

        Why include revoked rows?  Revoked tablets show up in the audit
        trail and the staff settings page wants to render them in a
        "Recently retired" section so an operator who revokes the wrong
        device can immediately see the mistake.  The serializer's
        ``is_revoked`` derived field carries the distinction.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Tablets
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Tablet'
          description: ''
  /api/v1/practice/{practice_uid}/tablets/{tablet_id}/clear/:
    post:
      operationId: practice_tablets_clear_create
      description: |-
        POST a 'reset to home screen' message to a paired tablet (api#760).

        Endpoint::

            POST /api/v1/practice/<practice_uid>/tablets/<tablet_id>/clear/

        Mirrors :class:`TabletRevokeView`'s permission + scoping pattern
        (manager+ on the practice) and reuses the channel-layer dispatch
        pattern :class:`TabletSendView` already wires up.  Body is empty.

        Why a separate endpoint instead of letting staff POST ``send/``
        with ``{"action": "clear_screen"}``?
        -----------------------------------
        The "Clear Tablet" Settings control should not require the web app
        to know about the action vocabulary -- the action verb is an
        implementation detail of the kiosk handshake.  A dedicated
        endpoint keeps the staff-side contract simple ("clear this
        tablet") and lets the api evolve the underlying WS verb without
        breaking the UI.  It also gives the audit log a distinct event
        type (``tablet.cleared`` vs. ``tablet.message_sent``) so operators
        can count clear actions independently of generic send activity.

        HIPAA contract
        --------------
        The WS message carries no PHI -- just the verb.  The audit event
        metadata carries the tablet id and the actor user only.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: tablet_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Tablets
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: >-
            Clear message enqueued onto the tablet's channel layer.  Returns 200
            (not 202) when the channel layer accepted the publish; the actual
            delivery to the device is still best-effort because Channels does
            not surface per-subscriber acks, but a successful publish is the
            strongest positive signal we can give the staff caller without a
            kiosk-side ack protocol.
        '202':
          description: >-
            The channel layer rejected the publish or was unconfigured (e.g.
            backed-up Redis, dev environment with the in-memory layer).  The
            audit event still fires so an operator can see the click; the kiosk
            simply did not receive the reset.
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: >-
            Tablet does not exist, belongs to a different practice, or has been
            revoked.  The three conditions surface the same 404 to avoid
            cross-tenant leakage.
  /api/v1/practice/{practice_uid}/tablets/{tablet_id}/form-session/submit/:
    post:
      operationId: practice_tablets_form_session_submit_create
      description: |-
        POST /api/v1/practice/<uid>/tablets/<tablet_id>/form-session/submit/.

        Tablet-authenticated.  See module docstring for the full contract.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: tablet_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Tablets
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FormSessionSubmitRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/FormSessionSubmitRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/FormSessionSubmitRequestRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormSessionSubmitResponse'
          description: ''
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormSessionSubmitResponse'
          description: ''
        '400':
          description: Malformed payload (bad JSON, bad signature base64).
        '401':
          description: Caller is not authenticated as a paired tablet.
        '403':
          description: >-
            Tablet is paired with a different practice, the session_id is not
            current for THIS tablet, or the template is not part of the
            session's bundle.


            Additionally, this endpoint returns 403 with
            ``code=inactive_practice`` or ``code=inactive_client`` when the
            practice or its parent client has been disabled.  See
            ``Inactive403Response`` for the body shape.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: >-
            Template (or its practice) is not addressable from this tablet's
            session.
  /api/v1/practice/{practice_uid}/tablets/{tablet_id}/rename/:
    post:
      operationId: practice_tablets_rename_create
      description: |-
        Rename an existing tablet without re-pairing.

        Staff manager+ surface.  The new name must be unique across
        *active* tablets at the practice -- same rule the model's
        conditional unique constraint enforces, but checked at write time
        so the API returns a clean 400 instead of an opaque 500.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: tablet_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Tablets
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TabletRenameRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/TabletRenameRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/TabletRenameRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tablet'
          description: ''
        '400':
          description: Name collision with another active tablet at the practice.
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Tablet not found.
  /api/v1/practice/{practice_uid}/tablets/{tablet_id}/revoke/:
    post:
      operationId: practice_tablets_revoke_create
      description: |-
        Soft-delete a tablet and revoke its OAuth tokens.

        POST-only -- the action is a state transition (active -> revoked),
        not a DELETE of the row.  Idempotent: re-revoking an already-
        revoked tablet returns the same 200 payload but does not double-
        stamp the timestamp.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: tablet_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Tablets
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tablet'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: Tablet not found at this practice.
  /api/v1/practice/{practice_uid}/tablets/{tablet_id}/send/:
    post:
      operationId: practice_tablets_send_create
      description: >-
        POST a staff-initiated push action to a paired tablet (api#684 /
        api#771).


        Endpoint::

            POST /api/v1/practice/<practice_uid>/tablets/<tablet_id>/send/
            { "action": "open_patient_intake",
              "payload": {"patient_id": "<uuid>", "patient_name": "Jane Doe"} }

        api#771 adds the ``form_session`` action, which uses a sibling

        ``form_session`` field instead of ``payload``::

            { "action": "form_session",
              "form_session": {
                "bundle_id": 42,
                "patient_id": "<uuid>",        // optional
                "appointment_id": "<uuid>"     // optional
              } }

        For ``form_session``, the view resolves the bundle's ordered

        template IDs at send time (so a later edit of the bundle does NOT

        mutate an in-flight session), mints a fresh ``session_id`` (UUID4)

        so the staff web can correlate ack/submission later, and persists

        the resolved frame onto :class:`PendingTabletAction` for delivery

        via the same persist-then-publish path the other verbs use.


        The view validates the action against

        :data:`apps.tablets.actions.TABLET_ACTIONS` -- unknown verbs are a

        400 and never reach the wire, so a misconfigured client can't trick

        the kiosk into reacting to a verb it doesn't know.


        Why 202 instead of 200?

        -----------------------

        The Channels group send only enqueues to the channel layer; the api

        has no way to confirm the tablet's socket actually received the

        message (it might be offline, mid-reconnect, or the message might

        be dropped on layer eviction).  202 ACCEPTED is the honest answer:

        "the api accepted the request; delivery is best-effort".  Clients

        that need a hard confirmation should subscribe to an ack event on

        the practice-events socket (future work).


        Why 404 for revoked/stale tablets instead of 410?

        -------------------------------------------------

        The four "not a valid target" cases -- doesn't exist, wrong

        practice, revoked, stale -- collapse to a single 404.  The

        response shape and status are uniform across all four so a caller

        probing for valid ids can't distinguish "doesn't exist" from

        "exists at another practice", which would leak cross-tenant

        membership.  410 is a tempting alternative for the revoked case

        but the same leakage argument applies: a caller learning that a

        specific tablet id IS revoked at this practice already had to know

        it exists, which is not staff-facing information for tablets they

        cannot administer.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: tablet_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Tablets
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TabletSendRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/TabletSendRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/TabletSendRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '202':
          description: >-
            Message accepted into the channel layer; the tablet may or may not
            be connected.  202 (not 200) makes the queue-and-forget semantics
            explicit.
        '400':
          description: Unknown action or malformed payload.
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '404':
          description: >-
            Tablet does not exist, belongs to a different practice, is revoked,
            or is stale.  All four conditions surface the same 404 so the
            response shape does not leak cross-tenant or lifecycle information
            to a caller probing for valid ids.
  /api/v1/practice/{practice_uid}/tablets/pair-token/:
    post:
      operationId: practice_tablets_pair_token_create
      description: |-
        Mint a short-lived single-use pair token for a new tablet.

        Staff-authenticated; manager+ role (writes against practice
        settings).  The response carries:

        * ``pair_token`` -- the raw JWT (only useful to a tablet capable
          of decoding it; included for non-QR flows like a future
          print-and-hand-off workflow).
        * ``qr_payload`` -- the absolute URL the tablet scans.  Web
          renders this as a QR code.
        * ``expires_at`` -- ISO-8601 UTC timestamp of the pair token's
          expiry (5 minutes after mint).  Used for the staff-facing
          countdown.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Tablets
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PairTokenCreateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PairTokenCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PairTokenCreateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PairTokenResponse'
          description: ''
        '400':
          description: Name collision with an existing active tablet.
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/user-grants/:
    get:
      operationId: practice_user_grants_list
      description: >-
        Returns every :class:`PracticeUserGrant` row attached to the practice. 
        Optional ``?user=<uuid>`` and ``?grant_type=<perm>`` filters narrow the
        result set; when both are supplied they are AND-ed.  ``grant_type``
        accepts the canonical permission string (e.g. ``patient.can_share``,
        ``reports.production``).
      summary: List per-practice user grants
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Staff
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PracticeUserGrantList'
          description: ''
    post:
      operationId: practice_user_grants_create
      description: >-
        Adds a new grant for the given user + permission pair.  The view sets
        ``practice`` from the URL and ``granted_by`` from the requesting user,
        so the body only carries the target user, the permission, an optional
        ``granted`` boolean (defaults True), and an optional free-text
        ``note``.  Returns the full grant on success.
      summary: Create a per-practice user grant
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Staff
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PracticeUserGrantCreateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PracticeUserGrantCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PracticeUserGrantCreateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PracticeUserGrantCreate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/user-grants/{grant_uid}/:
    delete:
      operationId: practice_user_grants_destroy
      description: >-
        Permanently removes the grant row.  The resolution helper in
        :mod:`apps.sharing.permissions` falls back to the role default once the
        row is gone, so a deleted grant returns the user to the role-default
        permission state.
      summary: Revoke a per-practice user grant
      parameters:
        - in: path
          name: grant_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Staff
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/voting/poll/:
    get:
      operationId: practice_voting_poll_list
      description: >-
        CRUD for practice polls with vote, results, and active-count actions.


        - **list**: All visible polls (unexpired + up to 12h after expiration).

        - **create**: OFFICE_MANAGER or ADMIN only.

        - **retrieve**: Any practice member.

        - **destroy**: Creator or ADMIN only.

        - **vote**: POST ``/poll/{id}/vote/`` with ``option_id``.

        - **results**: GET ``/poll/{id}/results/`` (403 if poll not yet
        expired).

        - **active_count**: GET ``/poll/active-count/`` for badge display.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Voting
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PollList'
          description: ''
    post:
      operationId: practice_voting_poll_create
      description: >-
        CRUD for practice polls with vote, results, and active-count actions.


        - **list**: All visible polls (unexpired + up to 12h after expiration).

        - **create**: OFFICE_MANAGER or ADMIN only.

        - **retrieve**: Any practice member.

        - **destroy**: Creator or ADMIN only.

        - **vote**: POST ``/poll/{id}/vote/`` with ``option_id``.

        - **results**: GET ``/poll/{id}/results/`` (403 if poll not yet
        expired).

        - **active_count**: GET ``/poll/active-count/`` for badge display.
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Voting
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PollCreateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PollCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PollCreateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PollCreate'
          description: ''
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/voting/poll/{id}/:
    get:
      operationId: practice_voting_poll_retrieve
      description: >-
        CRUD for practice polls with vote, results, and active-count actions.


        - **list**: All visible polls (unexpired + up to 12h after expiration).

        - **create**: OFFICE_MANAGER or ADMIN only.

        - **retrieve**: Any practice member.

        - **destroy**: Creator or ADMIN only.

        - **vote**: POST ``/poll/{id}/vote/`` with ``option_id``.

        - **results**: GET ``/poll/{id}/results/`` (403 if poll not yet
        expired).

        - **active_count**: GET ``/poll/active-count/`` for badge display.
      parameters:
        - in: path
          name: id
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Voting
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PollDetail'
          description: ''
    delete:
      operationId: practice_voting_poll_destroy
      description: >-
        CRUD for practice polls with vote, results, and active-count actions.


        - **list**: All visible polls (unexpired + up to 12h after expiration).

        - **create**: OFFICE_MANAGER or ADMIN only.

        - **retrieve**: Any practice member.

        - **destroy**: Creator or ADMIN only.

        - **vote**: POST ``/poll/{id}/vote/`` with ``option_id``.

        - **results**: GET ``/poll/{id}/results/`` (403 if poll not yet
        expired).

        - **active_count**: GET ``/poll/active-count/`` for badge display.
      parameters:
        - in: path
          name: id
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Voting
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
  /api/v1/practice/{practice_uid}/voting/poll/{id}/results/:
    get:
      operationId: practice_voting_poll_results_retrieve
      description: Return aggregated results.  Returns 403 if the poll has not yet expired.
      parameters:
        - in: path
          name: id
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Voting
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PollResults'
          description: ''
        '403':
          description: No response body
  /api/v1/practice/{practice_uid}/voting/poll/{id}/vote/:
    post:
      operationId: practice_voting_poll_vote_create
      description: Cast a vote on this poll.  Returns 409 if the user already voted.
      parameters:
        - in: path
          name: id
          schema:
            type: string
          required: true
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Voting
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VoteRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/VoteRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/VoteRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          description: No response body
        '403':
          description: >-
            Forbidden.


            Returned with ``code=inactive_practice`` when the practice's
            ``is_active`` flag is False, or ``code=inactive_client`` when the
            parent Client (organisation) is disabled and the cascade blocks
            every descendant write.  Reads on disabled tenants continue to
            succeed so internal support staff can triage; only mutating verbs
            (POST/PUT/PATCH/DELETE) hit this gate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inactive403Response'
              examples:
                inactivePractice:
                  summary: Practice is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_practice
                inactiveClient:
                  summary: Parent client/organization is disabled
                  value:
                    detail: This practice or its parent organization is disabled.
                    code: inactive_client
        '409':
          description: No response body
  /api/v1/practice/{practice_uid}/voting/poll/active-count/:
    get:
      operationId: practice_voting_poll_active_count_retrieve
      description: Return the count of unexpired polls for the practice (badge display).
      parameters:
        - in: path
          name: practice_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Voting
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/practice/available/:
    get:
      operationId: practice_available_list
      description: |-
        List practices the current user can upload to.

        Returns all active practices belonging to the user's client.
        Staff-only: patient users should not list practices.
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Practice'
          description: ''
  /api/v1/practice/create/:
    post:
      operationId: practice_create_create
      description: |-
        Create a new practice during import onboarding.

        If the authenticated user does not yet have a Client, one is created
        automatically using the practice name.  The new practice is then
        associated with the user's client.

        The ``import:write`` scope (legacy catalog entry, still issued to the
        first-party migration tools — escape-importer / escape-backup /
        escape-restore) gates this endpoint.  The new-taxonomy ``sync:*``
        scopes are reserved for a future tightening; until any first-party
        tool actually ships with a ``sync:patients`` token, requiring it
        here would lock the importer out of practice onboarding.
      tags:
        - Practices
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Practice'
          description: ''
        '402':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlanLimitError'
          description: Plan-limit rejection (practice or user cap exceeded).
  /api/v1/practice/mine/:
    get:
      operationId: practice_mine_retrieve
      description: >-
        Retrieve or update the user's default practice (first practice under
        their client).
      tags:
        - practice
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Practice'
          description: ''
    put:
      operationId: practice_mine_update
      description: >-
        Retrieve or update the user's default practice (first practice under
        their client).
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PracticeRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PracticeRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PracticeRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Practice'
          description: ''
    patch:
      operationId: practice_mine_partial_update
      description: >-
        Retrieve or update the user's default practice (first practice under
        their client).
      tags:
        - practice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPracticeRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPracticeRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPracticeRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Practice'
          description: ''
  /api/v1/practice/plans/:
    get:
      operationId: practice_plans_list
      description: |-
        List active subscription tiers, cheapest first.

        Public-readable: the marketing site renders pricing tiles from this
        endpoint, so AnonymousUser is allowed.  Pagination is disabled because
        the result set is bounded by the four canonical tiers and the frontend
        expects a flat array, not a paginated envelope.
      tags:
        - Plans
      security:
        - bearerAuth: []
        - oauth2: []
        - {}
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Plan'
          description: ''
  /api/v1/share/{share_uid}/:
    get:
      operationId: share_retrieve
      description: |-
        ``GET /api/v1/share/<share_uid>/`` -- landing-page payload.

        Requires a share-session JWT whose per-share audience matches the URL
        ``share_uid``.  Cross-share JWT reuse is rejected by
        :class:`SharePermission` with HTTP 403 so the recipient UI can
        distinguish "not logged in" (401) from "wrong share" (403).
      parameters:
        - in: path
          name: share_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - share
      security:
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/share/{share_uid}/download/:
    post:
      operationId: share_download_create
      description: |-
        ``POST /api/v1/share/<share_uid>/download/`` -- bundle URL.

        Returns ``{bundle_url, expires_in, status}``.

        * ``status="ready"``   -- a cached bundle exists and its object is
          still live in storage.  ``bundle_url`` is a 15-minute presigned
          GET URL (DO Spaces in prod, a filesystem path in local-dev).
        * ``status="pending"`` -- no matching bundle; a Celery task has been
          queued to build one.  Callers poll this endpoint until the status
          flips.

        On the happy path this endpoint also flips the share to
        ``downloaded`` status and records a ``bundle_downloaded`` audit
        event.  The status transition happens on the FIRST successful
        signed-URL issuance; subsequent polls against the same bundle don't
        re-flip the status (the model's helper short-circuits) but do
        emit additional audit events so the download timeline is complete.

        .. note::
           The bundle generator itself ships in api#174.  This endpoint
           assumes :func:`apps.sharing.tasks.generate_bundle` returns a
           Spaces object key on success; if the task is stubbed or failing
           locally we fall through to a filesystem-backed URL so the
           recipient at least sees something rather than a confusing 500.
      parameters:
        - in: path
          name: share_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - share
      security:
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/share/{share_uid}/import/:
    post:
      operationId: share_import_create
      description: >-
        Copy share items into the destination practice (api#175).


        **Auth**: regular staff-session JWT on ``Authorization`` AND a

        share-session JWT on ``X-Share-Token``.  The dual-credential requirement

        is what makes this the "authenticated scope" version of the share JWT:

        the caller has proven both that they are a known Escape user *and*

        that they hold a valid share-session token whose audience matches the

        share being imported.


        **Body**: see
        :class:`apps.sharing.serializers.ShareImportRequestSerializer`.


        **Per-item transaction**: each item is copied inside its own atomic

        block so a single bad UUID does not roll back the whole batch.  The

        response ``errors[]`` array carries a short non-PHI reason code for

        each failure.  The outer request is NOT transactional -- callers get

        partial success on purpose.


        **Audit**: on success, a single ``share.imported`` event is emitted

        with ``dest_practice_uid``, ``dest_patient_uid``, ``imported_count``.

        The share row is also transitioned to ``downloaded`` via

        :meth:`Share.mark_downloaded` so the sender-side My Shares UI shows

        the correct terminal state.
      parameters:
        - in: path
          name: share_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - share
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/share/{share_uid}/items/{item_uid}/:
    get:
      operationId: share_items_retrieve
      description: |-
        Render a single item in the share.

        Routing is resource-type aware:

        * **Document** -> streamed ``application/pdf`` (or the original
          mime if non-PDF).
        * **Imaging Exam** -> streamed ``application/dicom`` (first image of
          the exam; multi-image bundles go through ``/download/``).
        * **Perio Exam** / **Clinical Note** / **Patient Summary** ->
          ``application/json`` with the same shape as the legacy
          ``/items/<type>/<id>/`` endpoint so web#158 can consume either.

        The ``item_uid`` is looked up *inside the share* -- an item UUID that
        isn't listed in the share returns 404 rather than 200, even if it
        exists on the same patient.  Combined with the per-share JWT
        audience check this gives us defence in depth against JWT reuse.
      parameters:
        - in: path
          name: item_uid
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: share_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - share
      security:
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/share/redeem/authenticated/:
    post:
      operationId: share_redeem_authenticated_create
      description: |-
        Authenticated redemption -- caller proves staff session AND token.

        Uses DRF's default authentication stack so the staff session JWT on
        ``Authorization: Bearer ...`` is validated normally by SimpleJWT.  On
        top of that, the caller must still present a valid share token or
        Diceware phrase in the body.  Both checks must pass; a caller with
        only a session token sees a 400 (missing body field) rather than a
        free pass, and a caller with only a token is routed to the guest
        endpoint by the URL.

        The resulting JWT carries ``scope=authenticated`` and
        ``sub=user:<user_uid>`` so audit records can link the redemption to
        the Escape staff user who drove it.  The session JWT is not
        re-embedded -- spec §6 calls for re-issue via the staff session, but
        the details of that re-issue (e.g. auto-re-mint on JWT expiry from the
        portal UI) are outside the HTTP surface defined here.
      tags:
        - share
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ShareRedeemInputRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ShareRedeemInputRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ShareRedeemInputRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShareRedeemInput'
          description: ''
  /api/v1/share/redeem/guest/:
    post:
      operationId: share_redeem_guest_create
      description: |-
        Guest redemption -- anyone with a valid token / phrase may call.

        No session required.  The resulting JWT carries ``scope=guest`` and a
        hashed-IP ``sub`` claim so downstream audit records can distinguish
        guest traffic from authenticated-staff traffic.

        Rate limited to 5 per IP per 5 minutes per share_uid prefix via
        :class:`ShareSessionRedeemThrottle`; on top of that, 5 consecutive
        failures trip a 15-minute lockout tracked in the cache.
      tags:
        - share
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ShareRedeemInputRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ShareRedeemInputRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ShareRedeemInputRequest'
      security:
        - bearerAuth: []
        - oauth2: []
        - {}
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShareRedeemInput'
          description: ''
  /api/v1/support/import-issue/:
    get:
      operationId: support_import_issue_list
      description: >-
        Supports ``?status=``, ``?kind=``, ``?importer=``, ``?occurred_from=`` /
        ``?occurred_to=`` filters.  Ordered by ``-created`` (newest first). 
        Returns the compact list shape; open a detail view to read the full
        ``context`` payload.
      summary: List inbound import issues (staff-only).
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
      tags:
        - support
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedImportIssueListList'
          description: ''
    post:
      operationId: support_import_issue_create
      description: >-
        Accepts a structured failure report (schema_change / data_shape /
        runtime).  Authenticated by a first-party OAuth token carrying
        ``import:write`` scope -- the importer holds this token; the web app and
        third-party apps cannot reach this endpoint.  The request body may carry
        unredacted PHI in ``context``; rows are PHI-bearing and gated to staff
        users on the read side.
      summary: Receive an import-error report from an escape-importer instance.
      tags:
        - support
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportIssueCreateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ImportIssueCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ImportIssueCreateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportIssueCreate'
          description: ''
  /api/v1/support/import-issue/{issue_uid}/:
    get:
      operationId: support_import_issue_retrieve
      description: >-
        Returns the full report including the PHI-bearing ``context`` JSON. 
        Staff-only.
      summary: Read a single import issue (staff-only).
      parameters:
        - in: path
          name: issue_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - support
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportIssueDetail'
          description: ''
    patch:
      operationId: support_import_issue_partial_update
      description: >-
        Writable fields: ``status``, ``assigned_to``, ``resolution_note``.
        Everything else is read-only -- the originating report body is immutable
        after intake.
      summary: Update triage state on an import issue (staff-only).
      parameters:
        - in: path
          name: issue_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - support
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedImportIssueDetailRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedImportIssueDetailRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedImportIssueDetailRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportIssueDetail'
          description: ''
  /api/v1/support/sessions/:
    get:
      operationId: support_sessions_list
      description: |-
        ``GET`` lists past sessions (filterable); ``POST`` mints a new one.

        Both methods are restricted to support staff
        (:class:`apps.support.permissions.IsSupportStaff`) -- the audit
        list exposes the inverse side of the redemption surface and so
        inherits the same gate.
      summary: List support sessions (audit log).  Support-staff only.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
      tags:
        - Support
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedSupportSessionListList'
          description: ''
    post:
      operationId: support_sessions_create
      description: |-
        Mint a :class:`SupportSession` row + subscriber token.

        The row, the audit event, and the token mint are committed
        together inside an explicit ``transaction.atomic()`` block --
        a partial commit on a LiveKit-config failure would leave
        dangling sessions for which no token was ever issued.  The
        atomic block re-raises :class:`LiveKitNotConfigured`, which
        triggers a clean rollback; the outer ``try`` catches it after
        the rollback has run and surfaces the 503 response.
      summary: Mint a support code + subscriber token.  Support-staff only.
      tags:
        - Support
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportSessionCreateResponse'
          description: ''
        '503':
          description: LiveKit not configured.
  /api/v1/support/sessions/{session_id}/end/:
    post:
      operationId: support_sessions_end_create
      description: |-
        End an active session.  Idempotent on already-ended rows.

        Either the originating tech or the redeeming user may call; any
        other authenticated user gets 403.  The endpoint deliberately
        does not let an arbitrary support-staff member end someone
        else's session -- the audit story is cleaner if the actor is
        one of the two participants.
      summary: End a support session.  Either party may call.
      parameters:
        - in: path
          name: session_id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Support
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportSessionEndResponse'
          description: ''
        '403':
          description: Caller is neither the tech nor the user.
        '409':
          description: Session already ended.
  /api/v1/support/sessions/redeem/:
    post:
      operationId: support_sessions_redeem_create
      description: |-
        Validate the code, mark redeemed, mint publisher token.

        The endpoint runs two distinct transactions:

        1.  An "expired-or-bust" atomic block that picks up the row,
            checks the TTL, and -- if expired -- commits the EXPIRED
            transition + audit event before raising
            :class:`ValidationError`.  Without the split, a single
            outer atomic would roll back the EXPIRED save when the
            ValidationError propagates out, and the row would stay
            stuck in PENDING for the next reader.

        2.  A "redeem-or-bust" atomic block that flips the row to
            ACTIVE, mints the publisher token, and emits the
            REDEEMED audit event.  On
            :class:`LiveKitNotConfigured` the block rolls back so
            the user does not end up associated with a session it
            cannot connect to.
      summary: Redeem a support code.  Any authenticated user.
      tags:
        - Support
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SupportSessionRedeemRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/SupportSessionRedeemRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/SupportSessionRedeemRequestRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportSessionRedeemResponse'
          description: ''
        '400':
          description: Invalid, expired, or already-redeemed code.
        '503':
          description: LiveKit not configured.
  /api/v1/webhooks/events/:
    get:
      operationId: webhooks_events_retrieve
      description: >-
        Public endpoint returning all registered webhook events grouped by
        category.


        No authentication required — this is a discovery endpoint for developers

        building integrations against the Escape Dental API.
      tags:
        - Webhooks
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
        - {}
      responses:
        '200':
          description: No response body
  /api/v1/webhooks/subscriptions/:
    get:
      operationId: webhooks_subscriptions_list
      description: >-
        List and create webhook subscriptions.


        GET  — returns all subscriptions owned by the authenticated user's OAuth
        applications.

        POST — creates a new subscription (URL must pass validation, events must
        be known).
      tags:
        - webhooks
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/WebhookSubscription'
          description: ''
    post:
      operationId: webhooks_subscriptions_create
      description: >-
        List and create webhook subscriptions.


        GET  — returns all subscriptions owned by the authenticated user's OAuth
        applications.

        POST — creates a new subscription (URL must pass validation, events must
        be known).
      tags:
        - webhooks
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookSubscriptionCreateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/WebhookSubscriptionCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/WebhookSubscriptionCreateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscriptionCreate'
          description: ''
  /api/v1/webhooks/subscriptions/{subscription_uid}/:
    get:
      operationId: webhooks_subscriptions_retrieve
      description: >-
        Retrieve, update, or delete a single webhook subscription.


        Scoped to subscriptions owned by the authenticated user's OAuth
        applications.
      parameters:
        - in: path
          name: subscription_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - webhooks
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscription'
          description: ''
    put:
      operationId: webhooks_subscriptions_update
      description: >-
        Retrieve, update, or delete a single webhook subscription.


        Scoped to subscriptions owned by the authenticated user's OAuth
        applications.
      parameters:
        - in: path
          name: subscription_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - webhooks
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookSubscriptionCreateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/WebhookSubscriptionCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/WebhookSubscriptionCreateRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscriptionCreate'
          description: ''
    patch:
      operationId: webhooks_subscriptions_partial_update
      description: >-
        Retrieve, update, or delete a single webhook subscription.


        Scoped to subscriptions owned by the authenticated user's OAuth
        applications.
      parameters:
        - in: path
          name: subscription_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - webhooks
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedWebhookSubscriptionCreateRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedWebhookSubscriptionCreateRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedWebhookSubscriptionCreateRequest'
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscriptionCreate'
          description: ''
    delete:
      operationId: webhooks_subscriptions_destroy
      description: >-
        Retrieve, update, or delete a single webhook subscription.


        Scoped to subscriptions owned by the authenticated user's OAuth
        applications.
      parameters:
        - in: path
          name: subscription_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - webhooks
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '204':
          description: No response body
  /api/v1/webhooks/subscriptions/{subscription_uid}/deliveries/:
    get:
      operationId: webhooks_subscriptions_deliveries_list
      description: |-
        List delivery logs for a specific subscription.

        Paginated (50 per page) and scoped to subscriptions owned by the
        authenticated user's OAuth applications.
      parameters:
        - name: page
          required: false
          in: query
          description: A page number within the paginated result set.
          schema:
            type: integer
        - name: page_size
          required: false
          in: query
          description: Number of results to return per page.
          schema:
            type: integer
        - in: path
          name: subscription_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - webhooks
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedWebhookDeliveryList'
          description: ''
  /api/v1/webhooks/subscriptions/{subscription_uid}/test/:
    post:
      operationId: webhooks_subscriptions_test_create
      description: |-
        Send a test ping delivery to a subscription's target URL.

        Creates a WebhookDelivery with event type 'test.ping' and enqueues it
        for immediate delivery via Celery.
      parameters:
        - in: path
          name: subscription_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Webhooks
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/webhooks/subscriptions/{subscription_uid}/verify/:
    post:
      operationId: webhooks_subscriptions_verify_create
      description: >-
        Trigger URL ownership verification for a subscription.


        Enqueues a Celery task that sends a challenge request to the target URL.

        The subscription's is_verified flag is set to True only after the
        endpoint

        echoes the challenge back correctly.
      parameters:
        - in: path
          name: subscription_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Webhooks
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/xrays/sessions/{session_uid}/:
    get:
      operationId: xrays_sessions_retrieve
      description: |-
        Get capture session status for crash recovery.

        Returns session state, image count, and template info.
      parameters:
        - in: path
          name: session_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - xrays
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CaptureSessionStatus'
          description: ''
  /api/v1/xrays/sessions/{session_uid}/complete/:
    post:
      operationId: xrays_sessions_complete_create
      description: |-
        Mark a capture session as complete.

        Called by the desktop app when the user clicks "Complete".
        Sets ``Exam.is_complete = True``.
      parameters:
        - in: path
          name: session_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Imaging
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '200':
          description: No response body
  /api/v1/xrays/sessions/{session_uid}/images/:
    post:
      operationId: xrays_sessions_images_create
      description: |-
        Upload a single dental image to a capture session.

        Called by the desktop app after each image is captured via TWAIN/WIA.
      parameters:
        - in: path
          name: session_uid
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - xrays
      requestBody:
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/XrayImageUploadRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/XrayImageUploadRequest'
        required: true
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/XrayImageUpload'
          description: ''
  /api/v1/xrays/token-exchange/:
    post:
      operationId: xrays_token_exchange_create
      description: |-
        Exchange a one-time capture token for an OAuth2 access token.

        Called by the desktop app after receiving the ``escape-xrays://`` URL.
        No prior authentication is required — the token IS the auth.
      tags:
        - Imaging
      security:
        - jwtAuth: []
        - bearerAuth: []
        - oauth2: []
        - {}
      responses:
        '200':
          description: No response body
components:
  schemas:
    ARAgingPracticeRow:
      type: object
      description: Per-practice aging breakdown row.
      properties:
        practice_id:
          type: string
        practice_name:
          type: string
        total_ar:
          type: string
        current:
          type: string
        days_31_60:
          type: string
        days_61_90:
          type: string
        over_90:
          type: string
        patient_count:
          type: integer
      required:
        - current
        - days_31_60
        - days_61_90
        - over_90
        - patient_count
        - practice_id
        - practice_name
        - total_ar
    ARAgingResponse:
      type: object
      description: Top-level AR-aging response.
      properties:
        as_of:
          type: string
        totals:
          $ref: '#/components/schemas/_AgingBuckets'
        practices:
          type: array
          items:
            $ref: '#/components/schemas/ARAgingPracticeRow'
      required:
        - as_of
        - practices
        - totals
    Adjustment:
      type: object
      description: Read-only serializer for account adjustments.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        custom_data:
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        adj_date:
          type: string
          format: date
          readOnly: true
          nullable: true
        adj_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        adj_type:
          type: string
          readOnly: true
          maxLength: 100
        adj_note:
          type: string
          readOnly: true
        created_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        modified_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        practice:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        provider:
          type: string
          format: uuid
          readOnly: true
          nullable: true
      required:
        - adj_amount
        - adj_date
        - adj_note
        - adj_type
        - created
        - created_by
        - custom_data
        - id
        - modified
        - modified_by
        - patient
        - practice
        - provider
    AdjustmentWrite:
      type: object
      description: >-
        Create an adjustment record. Practice and patient are set by the view.


        GH-711: ``provider`` (and ``patient`` on the practice-wide bulk path)

        is validated against the URL practice via

        :class:`~apps.common.bulk.PracticeFKValidatorMixin`. 
        ``make_bulk_serializer``

        will inject ``patient`` for the bulk view; the mixin's dict picks it up

        via the dynamic subclass's identical ``practice_scoped_fk_fields`` value

        (the dict is inherited).
      properties:
        adj_date:
          type: string
          format: date
          nullable: true
        adj_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        adj_type:
          type: string
          maxLength: 100
        adj_note:
          type: string
        provider:
          type: string
          format: uuid
          nullable: true
        custom_data: {}
    AdjustmentWriteRequest:
      type: object
      description: >-
        Create an adjustment record. Practice and patient are set by the view.


        GH-711: ``provider`` (and ``patient`` on the practice-wide bulk path)

        is validated against the URL practice via

        :class:`~apps.common.bulk.PracticeFKValidatorMixin`. 
        ``make_bulk_serializer``

        will inject ``patient`` for the bulk view; the mixin's dict picks it up

        via the dynamic subclass's identical ``practice_scoped_fk_fields`` value

        (the dict is inherited).
      properties:
        adj_date:
          type: string
          format: date
          nullable: true
        adj_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        adj_type:
          type: string
          maxLength: 100
        adj_note:
          type: string
        provider:
          type: string
          format: uuid
          nullable: true
        custom_data: {}
    AdjustmentsPracticeRow:
      type: object
      description: Per-practice adjustments breakdown.
      properties:
        practice_id:
          type: string
        practice_name:
          type: string
        total_adjustments:
          type: string
        by_type:
          type: array
          items:
            $ref: '#/components/schemas/_AdjustmentType'
      required:
        - by_type
        - practice_id
        - practice_name
        - total_adjustments
    AdjustmentsResponse:
      type: object
      description: Response for ``/reports/adjustments/``.
      properties:
        date_from:
          type: string
        date_to:
          type: string
        grand_total:
          type: string
        practices:
          type: array
          items:
            $ref: '#/components/schemas/AdjustmentsPracticeRow'
      required:
        - date_from
        - date_to
        - grand_total
        - practices
    AdminAuditEventRow:
      type: object
      description: |-
        Single event row in the response payload.

        Mirrors the shape returned by :func:`apps.audit.reports.shape_event`
        so the support UI can reuse the same row component the
        right-of-access PDF/JSON renders.  Hand-rolled (not a
        ``ModelSerializer``) because the events live in Elasticsearch, not
        Postgres.
      properties:
        occurred_at:
          type: string
          description: ISO8601 UTC timestamp the event occurred.
        actor_user_email:
          type: string
        actor_user_name:
          type: string
        actor_role:
          type: string
        actor_ip:
          type: string
        actor_user_agent:
          type: string
        event_type:
          type: string
          description: Dotted-name event type (e.g. patient.viewed).
        event_category:
          type: string
        resource_type:
          type: string
        resource_id:
          type: string
        outcome:
          type: string
        purpose:
          type: string
          description: HIPAA purpose-of-disclosure bucket derived from event_type.
      required:
        - actor_ip
        - actor_role
        - actor_user_agent
        - actor_user_email
        - actor_user_name
        - event_category
        - event_type
        - occurred_at
        - outcome
        - purpose
        - resource_id
        - resource_type
    AdminClient:
      type: object
      description: Read-only response for admin client endpoints.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          readOnly: true
        is_active:
          type: boolean
          readOnly: true
      required:
        - id
        - is_active
        - name
    AdminMCPToolCatalog:
      type: object
      description: |-
        Single entry in the canonical MCP tool catalog.

        The catalog is the read-only list of every tool the dispatcher knows
        about; it's the source the support UI uses to render every row, even
        for tools that don't yet have a :class:`PracticeToolGate` for the
        practice in view.

        ``required_scopes`` is exposed as a list for JSON friendliness --
        the registry stores it as a tuple.
      properties:
        name:
          type: string
          description: Registry key (e.g. 'count_patients').
        description:
          type: string
          description: Human-readable tool description.
        required_scopes:
          type: array
          items:
            type: string
          description: OAuth scopes required to invoke the tool, in addition to mcp:read.
        pass_context:
          type: boolean
          description: True for write tools that need the acting user + granted scopes.
      required:
        - description
        - name
        - pass_context
        - required_scopes
    AdminMagicLinkRequestRequest:
      type: object
      description: Request payload for minting a magic link via the admin API.
      properties:
        email:
          type: string
          format: email
          minLength: 1
          description: Email address of the user to mint a link for.
        purpose:
          allOf:
            - $ref: '#/components/schemas/PurposeEnum'
          default: passkey_recovery
          description: |-
            Purpose of the magic link.

            * `passkey_recovery` - Passkey recovery
        expires_in_minutes:
          type: integer
          maximum: 1440
          minimum: 1
          default: 15
          description: 'Link TTL in minutes (1-1440). Default: 15.'
      required:
        - email
    AdminMagicLinkResponse:
      type: object
      description: Response payload after minting a magic link.
      properties:
        token:
          type: string
          description: One-time plaintext token (shown once, never stored).
        url:
          type: string
          format: uri
          description: Full verify URL to hand to the user.
        expires_at:
          type: string
          format: date-time
          description: UTC expiry time.
        user_email:
          type: string
          format: email
          description: Email of the user the link was minted for.
      required:
        - expires_at
        - token
        - url
        - user_email
    AdminOAuthApp:
      type: object
      description: |-
        Read-only response for the admin OAuth app list and detail views.

        The shape is hand-rolled (rather than a ``ModelSerializer``) because
        the response stitches together fields from
        :class:`oauth2_provider.Application` and its
        :class:`apps.oauth.models.OAuthAppProfile` one-to-one row, plus a
        short rolling-window throttle summary.  Keeping the surface flat
        means the support UI (escape-support#48) doesn't have to traverse
        nested objects to render a row.

        The ``client_id`` field is the public identifier used everywhere
        else in the platform (Nexus, ``OAuthAppUsageMetric.scope_id`` for
        app-scoped rows) — Application's integer ``pk`` is deliberately
        not exposed.
      properties:
        client_id:
          type: string
          description: Public OAuth client identifier.
        name:
          type: string
          description: Human-readable application name.
        throttle_mode:
          type: string
          description: >-
            Per-app throttle override. ``default`` inherits the global
            OAUTH_THROTTLE_MODE; ``audit`` / ``enforce`` override it.
        is_first_party:
          type: boolean
          description: True for trusted internal apps (escape-web, importers).
        approval_status:
          type: string
          description: Approval workflow state (pending / approved / rejected / suspended).
        request_count_24h:
          type: integer
          description: Sum of request_count from OAuthAppUsageMetric over the last 24h.
        audit_throttle_hits_24h:
          type: integer
          description: >-
            Count of audit-mode throttle trips in the last 24h. Sourced from the
            ``oauth_app.throttle.audit_hit`` events emitted by the throttle
            layer into the ``audit-events-*`` ES index.
        enforce_throttle_hits_24h:
          type: integer
          description: >-
            Count of enforced 429 responses in the last 24h. Sourced from the
            ``oauth_app.throttle.enforce_reject`` events emitted by the throttle
            layer into the ``audit-events-*`` ES index.
        slots:
          type: array
          items:
            type: string
          description: Slot names this app declares. See SLOT_CHOICES.
        url_template:
          type: string
          default: ''
          description: >-
            URL template opened when the user clicks the slot tile. Recognised
            placeholders: {patient_id}, {practice_id}.
        approved_at:
          type: string
          format: date-time
          nullable: true
          description: >-
            When the app was last approved. None if never approved via the
            approval workflow.
        approved_by_email:
          type: string
          nullable: true
          description: >-
            Email of the staff user who last approved the app. None if never
            approved via the workflow.
      required:
        - approval_status
        - audit_throttle_hits_24h
        - client_id
        - enforce_throttle_hits_24h
        - is_first_party
        - name
        - request_count_24h
        - throttle_mode
    AdminPractice:
      type: object
      description: Read-only response for admin practice endpoints.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          readOnly: true
        is_active:
          type: boolean
          readOnly: true
      required:
        - id
        - is_active
        - name
    AdminPracticeToolGate:
      type: object
      description: |-
        Read serializer for a single :class:`PracticeToolGate` row.

        Returned by the list endpoint and as the response body of the PUT
        upsert endpoint.  ``created_by`` / ``modified_by`` are surfaced as
        actor emails so the support UI can render "disabled by alice@…"
        without an extra fetch.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        tool_key:
          type: string
          readOnly: true
          description: Registry key of the gated tool (e.g. 'count_patients').
        enabled:
          type: boolean
          readOnly: true
          description: >-
            When False, the tool is hidden from tools/list and rejected with
            JSON-RPC -32601 on tools/call. Default True so the row's mere
            presence is informational (e.g. an explicit re-enable record).
        note:
          type: string
          readOnly: true
          description: Optional reason for the gating, surfaced in the support UI.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        created_by_email:
          type: string
          nullable: true
          description: Return the email of the user who created this row, if known.
          readOnly: true
        modified_by_email:
          type: string
          nullable: true
          description: Return the email of the user who last modified this row, if known.
          readOnly: true
      required:
        - created
        - created_by_email
        - enabled
        - id
        - modified
        - modified_by_email
        - note
        - tool_key
    AdminPracticeToolGateUpsertRequest:
      type: object
      description: |-
        Request payload for the PUT upsert endpoint.

        Only ``enabled`` and ``note`` are settable; ``tool_key`` is taken
        from the URL.  ``note`` defaults to an empty string so a caller
        flipping the toggle without providing context is still a valid
        request (the audit log captures the actor + timestamp regardless).
      properties:
        enabled:
          type: boolean
          description: >-
            True to allow the tool, False to hide it from the practice's
            catalog.
        note:
          type: string
          default: ''
          description: Optional free-text reason surfaced in the support UI.
          maxLength: 4096
      required:
        - enabled
    AdminUser:
      type: object
      description: Read-only response for admin user endpoints.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        email:
          type: string
          format: email
          readOnly: true
          title: Email address
        first_name:
          type: string
          readOnly: true
        last_name:
          type: string
          readOnly: true
        is_active:
          type: boolean
          readOnly: true
      required:
        - email
        - first_name
        - id
        - is_active
        - last_name
    AgentEnum:
      enum:
        - lidocaine_epi
        - lidocaine
        - articaine_epi
        - articaine_epi_200
        - mepivacaine_epi
        - mepivacaine
        - bupivacaine_epi
        - prilocaine_epi
        - prilocaine
        - topical_benzocaine
        - topical_lidocaine
        - nitrous
        - other
      type: string
      description: |-
        * `lidocaine_epi` - Lidocaine 2% with Epinephrine 1:100,000
        * `lidocaine` - Lidocaine 2% (plain)
        * `articaine_epi` - Articaine 4% with Epinephrine 1:100,000
        * `articaine_epi_200` - Articaine 4% with Epinephrine 1:200,000
        * `mepivacaine_epi` - Mepivacaine 2% with Levonordefrin 1:20,000
        * `mepivacaine` - Mepivacaine 3% (plain)
        * `bupivacaine_epi` - Bupivacaine 0.5% with Epinephrine 1:200,000
        * `prilocaine_epi` - Prilocaine 4% with Epinephrine 1:200,000
        * `prilocaine` - Prilocaine 4% (plain)
        * `topical_benzocaine` - Topical Benzocaine 20%
        * `topical_lidocaine` - Topical Lidocaine 5%
        * `nitrous` - Nitrous Oxide / Oxygen
        * `other` - Other
    AgingReportInsuranceRow:
      type: object
      description: Per-carrier aging breakdown row.
      properties:
        carrier_id:
          type: string
          nullable: true
        carrier_name:
          type: string
        bal_0_30:
          type: string
        bal_31_60:
          type: string
        bal_61_90:
          type: string
        bal_over_90:
          type: string
        balance_total:
          type: string
      required:
        - bal_0_30
        - bal_31_60
        - bal_61_90
        - bal_over_90
        - balance_total
        - carrier_id
        - carrier_name
    AgingReportProviderRow:
      type: object
      description: Per-provider aging breakdown row.
      properties:
        provider_id:
          type: string
          nullable: true
        provider_name:
          type: string
        bal_0_30:
          type: string
        bal_31_60:
          type: string
        bal_61_90:
          type: string
        bal_over_90:
          type: string
        balance_total:
          type: string
      required:
        - bal_0_30
        - bal_31_60
        - bal_61_90
        - bal_over_90
        - balance_total
        - provider_id
        - provider_name
    AgingReportResponse:
      type: object
      description: Response from ``GET /reports/aging/``.
      properties:
        summary:
          $ref: '#/components/schemas/AgingReportSummary'
        top_balances:
          type: array
          items:
            $ref: '#/components/schemas/AgingReportTopBalance'
        by_provider:
          type: array
          items:
            $ref: '#/components/schemas/AgingReportProviderRow'
        by_insurance:
          type: array
          items:
            $ref: '#/components/schemas/AgingReportInsuranceRow'
      required:
        - by_insurance
        - by_provider
        - summary
        - top_balances
    AgingReportSummary:
      type: object
      description: Practice-wide aging summary block.
      properties:
        total_ar:
          type: string
        current:
          type: string
        days_31_60:
          type: string
        days_61_90:
          type: string
        over_90:
          type: string
        patient_count:
          type: integer
      required:
        - current
        - days_31_60
        - days_61_90
        - over_90
        - patient_count
        - total_ar
    AgingReportTopBalance:
      type: object
      description: One entry in the ``top_balances`` list.
      properties:
        patient_id:
          type: string
        patient_name:
          type: string
        balance_total:
          type: string
        bal_0_30:
          type: string
        bal_31_60:
          type: string
        bal_61_90:
          type: string
        bal_over_90:
          type: string
        last_payment_date:
          type: string
          nullable: true
        last_payment_amount:
          type: string
          nullable: true
      required:
        - bal_0_30
        - bal_31_60
        - bal_61_90
        - bal_over_90
        - balance_total
        - last_payment_amount
        - last_payment_date
        - patient_id
        - patient_name
    AnesthesiaRecord:
      type: object
      description: Read serializer for anesthesia records with denormalized display fields.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        appointment:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        provider:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        provider_name:
          type: string
          nullable: true
          description: Return the provider's display name, or None if no provider is set.
          readOnly: true
        agent:
          allOf:
            - $ref: '#/components/schemas/AgentEnum'
          readOnly: true
        agent_display:
          type: string
          description: Return the human-readable anesthetic agent label.
          readOnly: true
        agent_other:
          type: string
          readOnly: true
          description: If agent=other, specify here
        route:
          allOf:
            - $ref: '#/components/schemas/RouteEnum'
          readOnly: true
        route_display:
          type: string
          description: Return the human-readable route label.
          readOnly: true
        site:
          type: string
          readOnly: true
          description: 'e.g. #14 buccal infiltration, right IAN block'
        carpules:
          type: string
          format: decimal
          pattern: ^-?\d{0,2}(?:\.\d{0,1})?$
          readOnly: true
          description: Number of carpules administered
        lot_number:
          type: string
          readOnly: true
        expiration_date:
          type: string
          format: date
          readOnly: true
          nullable: true
        administered_at:
          type: string
          format: date-time
          readOnly: true
          description: When the anesthesia was administered
        note:
          type: string
          readOnly: true
          description: Complications, reactions, or additional notes
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - administered_at
        - agent
        - agent_display
        - agent_other
        - appointment
        - carpules
        - created
        - expiration_date
        - id
        - lot_number
        - modified
        - note
        - patient
        - provider
        - provider_name
        - route
        - route_display
        - site
    AnesthesiaRecordWrite:
      type: object
      description: |-
        Write serializer for creating and updating anesthesia records.

        The ``patient`` field is excluded because it is set from the URL path
        by the view's ``perform_create`` method.
      properties:
        appointment:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        agent:
          $ref: '#/components/schemas/AgentEnum'
        agent_other:
          type: string
          description: If agent=other, specify here
          maxLength: 100
        route:
          $ref: '#/components/schemas/RouteEnum'
        site:
          type: string
          description: 'e.g. #14 buccal infiltration, right IAN block'
          maxLength: 100
        carpules:
          type: string
          format: decimal
          pattern: ^-?\d{0,2}(?:\.\d{0,1})?$
          description: Number of carpules administered
        lot_number:
          type: string
          maxLength: 50
        expiration_date:
          type: string
          format: date
          nullable: true
        administered_at:
          type: string
          format: date-time
          description: When the anesthesia was administered
        note:
          type: string
          description: Complications, reactions, or additional notes
        custom_data: {}
      required:
        - administered_at
        - agent
    AnesthesiaRecordWriteRequest:
      type: object
      description: |-
        Write serializer for creating and updating anesthesia records.

        The ``patient`` field is excluded because it is set from the URL path
        by the view's ``perform_create`` method.
      properties:
        appointment:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        agent:
          $ref: '#/components/schemas/AgentEnum'
        agent_other:
          type: string
          description: If agent=other, specify here
          maxLength: 100
        route:
          $ref: '#/components/schemas/RouteEnum'
        site:
          type: string
          description: 'e.g. #14 buccal infiltration, right IAN block'
          maxLength: 100
        carpules:
          type: string
          format: decimal
          pattern: ^-?\d{0,2}(?:\.\d{0,1})?$
          description: Number of carpules administered
        lot_number:
          type: string
          maxLength: 50
        expiration_date:
          type: string
          format: date
          nullable: true
        administered_at:
          type: string
          format: date-time
          description: When the anesthesia was administered
        note:
          type: string
          description: Complications, reactions, or additional notes
        custom_data: {}
      required:
        - administered_at
        - agent
    ApiStatsRow:
      type: object
      description: |-
        Single aggregated row in the api-stats response.

        Each row is the sum of ``request_count`` over every persisted
        :class:`OAuthAppUsageMetric` whose ``bucket_start`` truncates to the
        same ``window_start`` for the same scope and method bucket.  Rows
        are produced server-side by the view; this serializer only shapes
        the response payload.
      properties:
        scope:
          $ref: '#/components/schemas/ScopeEnum'
        scope_id:
          type: string
        window_start:
          type: string
          format: date-time
          description: UTC start of the aggregation bucket.
        window_end:
          type: string
          format: date-time
          description: UTC end of the aggregation bucket (exclusive upper bound).
        method:
          type: string
          description: '`GET` (safe methods) or `WRITE` (POST/PUT/PATCH/DELETE).'
        count:
          type: integer
          description: Sum of request_count across rows in the bucket.
        app_name:
          type: string
          nullable: true
          description: OAuth Application.name for app-scoped rows; null for other scopes.
      required:
        - count
        - method
        - scope
        - scope_id
        - window_end
        - window_start
    AppLaunchResponse:
      type: object
      description: |-
        Response body for the audited launcher click-through endpoint.

        The web client posts to ``.../<client_id>/launch/`` and uses the
        returned ``target_url`` to ``window.open(url, '_blank',
        'noopener,noreferrer')``.  The round-trip is what lets us record
        an ``oauth_app.launch`` audit event per click.
      properties:
        target_url:
          type: string
          format: uri
          description: Where the browser should navigate to open the third-party app.
      required:
        - target_url
    AppLaunchTokenRequestRequest:
      type: object
      description: POST body for the launch-token create endpoint.
      properties:
        application:
          type: string
          minLength: 1
          description: OAuth2 Application.client_id of the 3rd-party app to launch.
        practice:
          type: string
          format: uuid
          description: UUID of the practice the launch will be scoped to.
      required:
        - application
        - practice
    AppLaunchTokenResponse:
      type: object
      description: Response body for a successful launch-token mint.
      properties:
        launch_token:
          type: string
          description: >-
            Opaque JWT to append as ``escape_token`` on the redirect to the
            3rd-party app.
        expires_in:
          type: integer
          description: >-
            Seconds until the token expires.  Callers should redirect
            immediately.
      required:
        - expires_in
        - launch_token
    AppVisibilityToggle:
      type: object
      description: PATCH body for toggling an app's visibility in a practice's navbar.
      properties:
        is_visible:
          type: boolean
      required:
        - is_visible
    Appointment:
      type: object
      description: |-
        Read/write serializer for appointments.

        GH-711: the ``patient`` / ``provider`` / ``provider_hygienist`` /
        ``operatory`` / ``treatment_plan`` FKs are validated against the URL
        practice on POST/PATCH.  Inherited by the bulk variant
        (:class:`AppointmentBulkWriteSerializer`) so the first-party bulk path
        can't drop an appointment under practice A's URL with practice B's
        patient/provider/operatory.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        provider_hygienist:
          type: string
          format: uuid
          nullable: true
        operatory:
          type: string
          format: uuid
          nullable: true
        treatment_plan:
          type: string
          format: uuid
          nullable: true
          description: Treatment plan this appointment was scheduled from
        date_time:
          type: string
          format: date-time
          nullable: true
        duration_minutes:
          type: integer
          maximum: 2147483647
          minimum: 0
        status:
          allOf:
            - $ref: '#/components/schemas/AppointmentStatusEnum'
          readOnly: true
        confirmed_status:
          oneOf:
            - $ref: '#/components/schemas/ConfirmedStatusEnum'
            - $ref: '#/components/schemas/BlankEnum'
        appointment_type:
          type: string
          description: e.g. New Patient Exam, Crown Prep
          maxLength: 100
        is_hygiene:
          type: boolean
        is_new_patient:
          type: boolean
        procedure_description:
          type: string
        note:
          type: string
        cancellation_reason:
          type: string
          description: Patient-supplied reason for cancellation
        reschedule_requested:
          type: boolean
          description: Patient asked the front desk to reschedule
        arrived_at:
          type: string
          format: date-time
          nullable: true
        seated_at:
          type: string
          format: date-time
          nullable: true
        ready_for_doctor_at:
          type: string
          format: date-time
          nullable: true
          description: When hygienist signaled ready for doctor exam
        ready_for_walkout_at:
          type: string
          format: date-time
          nullable: true
          description: When patient is ready for walkout (checkout/scheduling)
        dismissed_at:
          type: string
          format: date-time
          nullable: true
        dollars_scheduled:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Scheduled procedure total from the source PMS; nullable when unknown
        patient_name:
          type: string
          readOnly: true
        provider_name:
          type: string
          readOnly: true
        provider_hygienist_name:
          type: string
          readOnly: true
        cancelled_by:
          type: string
          nullable: true
          description: >-
            Username (email) of the staff user who cancelled the appointment.


            Sourced from the annotation on the list queryset
            (``_cancelled_by_email``

            subquery in :meth:`AppointmentListView.get_queryset`).  Returns
            ``None``

            for non-cancelled rows, for appointments that were cancelled before
            the

            status-log was introduced, or when the detail endpoint is used (the

            annotation is list-only).
          readOnly: true
        cancelled_by_name:
          type: string
          nullable: true
          description: |-
            Display name of the staff user who cancelled the appointment.

            Built from the first_name/last_name subquery annotations; falls back
            to the email annotation when both name fields are blank, mirroring
            :class:`AppointmentStatusLogSerializer.get_changed_by_name`.
          readOnly: true
        scheduled_procedures:
          type: array
          items:
            $ref: '#/components/schemas/AppointmentProcedure'
          readOnly: true
        custom_data: {}
      required:
        - cancelled_by
        - cancelled_by_name
        - id
        - patient_name
        - provider_hygienist_name
        - provider_name
        - scheduled_procedures
        - status
    AppointmentConfirmRequest:
      type: object
      description: Updates appointment confirmation status.
      properties:
        confirmed_status:
          $ref: '#/components/schemas/ConfirmedStatusEnum'
      required:
        - confirmed_status
    AppointmentCreate:
      type: object
      description: >-
        Used for creating appointments — allows setting initial status and
        scheduled procedures.


        Pass ``allow_overbooking: true`` to bypass scheduling conflict
        validation

        (used by import/sync tools for historical data, and by the web app when

        the user explicitly accepts a double-booking warning).


        ``status`` is intentionally writable here so the web app can create an

        appointment in a non-``scheduled`` initial state (e.g. a walk-in that

        already finished).  For *updates* by staff, use

        :class:`AppointmentUpdateSerializer`, which pins status read-only and

        forces status changes through the ``/transition/`` endpoint.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        provider_hygienist:
          type: string
          format: uuid
          nullable: true
        operatory:
          type: string
          format: uuid
          nullable: true
        treatment_plan:
          type: string
          format: uuid
          nullable: true
          description: Treatment plan this appointment was scheduled from
        date_time:
          type: string
          format: date-time
          nullable: true
        duration_minutes:
          type: integer
          maximum: 2147483647
          minimum: 0
        status:
          $ref: '#/components/schemas/AppointmentStatusEnum'
        confirmed_status:
          oneOf:
            - $ref: '#/components/schemas/ConfirmedStatusEnum'
            - $ref: '#/components/schemas/BlankEnum'
        appointment_type:
          type: string
          description: e.g. New Patient Exam, Crown Prep
          maxLength: 100
        is_hygiene:
          type: boolean
        is_new_patient:
          type: boolean
        procedure_description:
          type: string
        note:
          type: string
        cancellation_reason:
          type: string
          description: Patient-supplied reason for cancellation
        reschedule_requested:
          type: boolean
          description: Patient asked the front desk to reschedule
        arrived_at:
          type: string
          format: date-time
          nullable: true
        seated_at:
          type: string
          format: date-time
          nullable: true
        ready_for_doctor_at:
          type: string
          format: date-time
          nullable: true
          description: When hygienist signaled ready for doctor exam
        ready_for_walkout_at:
          type: string
          format: date-time
          nullable: true
          description: When patient is ready for walkout (checkout/scheduling)
        dismissed_at:
          type: string
          format: date-time
          nullable: true
        dollars_scheduled:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Scheduled procedure total from the source PMS; nullable when unknown
        patient_name:
          type: string
          readOnly: true
        provider_name:
          type: string
          readOnly: true
        provider_hygienist_name:
          type: string
          readOnly: true
        cancelled_by:
          type: string
          nullable: true
          description: >-
            Username (email) of the staff user who cancelled the appointment.


            Sourced from the annotation on the list queryset
            (``_cancelled_by_email``

            subquery in :meth:`AppointmentListView.get_queryset`).  Returns
            ``None``

            for non-cancelled rows, for appointments that were cancelled before
            the

            status-log was introduced, or when the detail endpoint is used (the

            annotation is list-only).
          readOnly: true
        cancelled_by_name:
          type: string
          nullable: true
          description: |-
            Display name of the staff user who cancelled the appointment.

            Built from the first_name/last_name subquery annotations; falls back
            to the email annotation when both name fields are blank, mirroring
            :class:`AppointmentStatusLogSerializer.get_changed_by_name`.
          readOnly: true
        scheduled_procedures:
          type: array
          items:
            $ref: '#/components/schemas/AppointmentProcedure'
        custom_data: {}
        allow_overbooking:
          type: boolean
          default: false
        skip_validation:
          type: boolean
          default: false
      required:
        - cancelled_by
        - cancelled_by_name
        - id
        - patient_name
        - provider_hygienist_name
        - provider_name
    AppointmentCreateRequest:
      type: object
      description: >-
        Used for creating appointments — allows setting initial status and
        scheduled procedures.


        Pass ``allow_overbooking: true`` to bypass scheduling conflict
        validation

        (used by import/sync tools for historical data, and by the web app when

        the user explicitly accepts a double-booking warning).


        ``status`` is intentionally writable here so the web app can create an

        appointment in a non-``scheduled`` initial state (e.g. a walk-in that

        already finished).  For *updates* by staff, use

        :class:`AppointmentUpdateSerializer`, which pins status read-only and

        forces status changes through the ``/transition/`` endpoint.
      properties:
        patient:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        provider_hygienist:
          type: string
          format: uuid
          nullable: true
        operatory:
          type: string
          format: uuid
          nullable: true
        treatment_plan:
          type: string
          format: uuid
          nullable: true
          description: Treatment plan this appointment was scheduled from
        date_time:
          type: string
          format: date-time
          nullable: true
        duration_minutes:
          type: integer
          maximum: 2147483647
          minimum: 0
        status:
          $ref: '#/components/schemas/AppointmentStatusEnum'
        confirmed_status:
          oneOf:
            - $ref: '#/components/schemas/ConfirmedStatusEnum'
            - $ref: '#/components/schemas/BlankEnum'
        appointment_type:
          type: string
          description: e.g. New Patient Exam, Crown Prep
          maxLength: 100
        is_hygiene:
          type: boolean
        is_new_patient:
          type: boolean
        procedure_description:
          type: string
        note:
          type: string
        cancellation_reason:
          type: string
          description: Patient-supplied reason for cancellation
        reschedule_requested:
          type: boolean
          description: Patient asked the front desk to reschedule
        arrived_at:
          type: string
          format: date-time
          nullable: true
        seated_at:
          type: string
          format: date-time
          nullable: true
        ready_for_doctor_at:
          type: string
          format: date-time
          nullable: true
          description: When hygienist signaled ready for doctor exam
        ready_for_walkout_at:
          type: string
          format: date-time
          nullable: true
          description: When patient is ready for walkout (checkout/scheduling)
        dismissed_at:
          type: string
          format: date-time
          nullable: true
        dollars_scheduled:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Scheduled procedure total from the source PMS; nullable when unknown
        scheduled_procedures:
          type: array
          items:
            $ref: '#/components/schemas/AppointmentProcedureRequest'
        custom_data: {}
        allow_overbooking:
          type: boolean
          default: false
        skip_validation:
          type: boolean
          default: false
    AppointmentProcedure:
      type: object
      description: Serializer for procedure codes linked to an appointment.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        procedure_code:
          type: string
          format: uuid
          nullable: true
        proc_code:
          type: string
          description: Denormalized procedure code for display
          maxLength: 20
        description:
          type: string
          maxLength: 255
        fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          description: Fee at time of scheduling
        tooth_num:
          type: string
          maxLength: 10
        surface:
          type: string
          maxLength: 20
        status:
          $ref: '#/components/schemas/AppointmentProcedureStatusEnum'
        custom_data: {}
      required:
        - id
        - proc_code
    AppointmentProcedureRequest:
      type: object
      description: Serializer for procedure codes linked to an appointment.
      properties:
        procedure_code:
          type: string
          format: uuid
          nullable: true
        proc_code:
          type: string
          minLength: 1
          description: Denormalized procedure code for display
          maxLength: 20
        description:
          type: string
          maxLength: 255
        fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          description: Fee at time of scheduling
        tooth_num:
          type: string
          maxLength: 10
        surface:
          type: string
          maxLength: 20
        status:
          $ref: '#/components/schemas/AppointmentProcedureStatusEnum'
        custom_data: {}
      required:
        - proc_code
    AppointmentProcedureStatusEnum:
      enum:
        - planned
        - completed
        - not_performed
      type: string
      description: |-
        * `planned` - Planned
        * `completed` - Completed
        * `not_performed` - Not Performed
    AppointmentStatus:
      type: object
      properties:
        slug:
          type: string
          readOnly: true
        label:
          type: string
          readOnly: true
        color:
          type: string
          readOnly: true
          description: Tailwind color name, e.g. teal, green, red
        is_visible:
          type: boolean
          readOnly: true
          description: Show appointments with this status on schedule views
        blocks_slot:
          type: boolean
          readOnly: true
          description: Prevent slot finder from booking over this status
        sort_order:
          type: integer
          readOnly: true
      required:
        - blocks_slot
        - color
        - is_visible
        - label
        - slug
        - sort_order
    AppointmentStatusEnum:
      enum:
        - scheduled
        - arrived
        - seated
        - ready_for_doctor
        - ready_for_walkout
        - complete
        - broken
        - cancelled
        - unscheduled
        - planned
        - other
        - note
      type: string
      description: |-
        * `scheduled` - Scheduled
        * `arrived` - Arrived
        * `seated` - Seated
        * `ready_for_doctor` - Ready for Doctor
        * `ready_for_walkout` - Ready for Walkout
        * `complete` - Complete
        * `broken` - Broken/No-Show
        * `cancelled` - Cancelled
        * `unscheduled` - Unscheduled
        * `planned` - Planned
        * `other` - Other
        * `note` - Note
    AppointmentStatusLog:
      type: object
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        previous_status:
          type: string
          maxLength: 20
        new_status:
          type: string
          maxLength: 20
        changed_by:
          type: string
          format: uuid
          nullable: true
        changed_by_name:
          type: string
          nullable: true
          readOnly: true
        changed_at:
          type: string
          format: date-time
          readOnly: true
        note:
          type: string
        custom_data: {}
      required:
        - changed_at
        - changed_by_name
        - id
        - new_status
        - previous_status
    AppointmentTransitionRequest:
      type: object
      description: Validates an appointment status transition request.
      properties:
        status:
          $ref: '#/components/schemas/AppointmentStatusEnum'
        note:
          type: string
          default: ''
      required:
        - status
    AppointmentUpdate:
      type: object
      description: >-
        Staff PUT/PATCH serializer — pins ``status`` read-only.


        Staff UIs must route status changes through the ``/transition/``
        endpoint,

        which enforces :data:`apps.scheduling.services.ALLOWED_TRANSITIONS` and

        writes an :class:`AppointmentStatusLog` row.  This serializer removes

        ``status`` from the writable field set so a stray ``PATCH {"status":
        ...}``

        from the web app is silently ignored rather than bypassing the state

        machine.


        The bulk-import path intentionally uses

        :class:`AppointmentBulkWriteSerializer` instead — the source PMS is

        authoritative for status and must be able to write it directly.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        provider_hygienist:
          type: string
          format: uuid
          nullable: true
        operatory:
          type: string
          format: uuid
          nullable: true
        treatment_plan:
          type: string
          format: uuid
          nullable: true
          description: Treatment plan this appointment was scheduled from
        date_time:
          type: string
          format: date-time
          nullable: true
        duration_minutes:
          type: integer
          maximum: 2147483647
          minimum: 0
        status:
          allOf:
            - $ref: '#/components/schemas/AppointmentStatusEnum'
          readOnly: true
        confirmed_status:
          oneOf:
            - $ref: '#/components/schemas/ConfirmedStatusEnum'
            - $ref: '#/components/schemas/BlankEnum'
        appointment_type:
          type: string
          description: e.g. New Patient Exam, Crown Prep
          maxLength: 100
        is_hygiene:
          type: boolean
        is_new_patient:
          type: boolean
        procedure_description:
          type: string
        note:
          type: string
        cancellation_reason:
          type: string
          description: Patient-supplied reason for cancellation
        reschedule_requested:
          type: boolean
          description: Patient asked the front desk to reschedule
        arrived_at:
          type: string
          format: date-time
          nullable: true
        seated_at:
          type: string
          format: date-time
          nullable: true
        ready_for_doctor_at:
          type: string
          format: date-time
          nullable: true
          description: When hygienist signaled ready for doctor exam
        ready_for_walkout_at:
          type: string
          format: date-time
          nullable: true
          description: When patient is ready for walkout (checkout/scheduling)
        dismissed_at:
          type: string
          format: date-time
          nullable: true
        dollars_scheduled:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Scheduled procedure total from the source PMS; nullable when unknown
        patient_name:
          type: string
          readOnly: true
        provider_name:
          type: string
          readOnly: true
        provider_hygienist_name:
          type: string
          readOnly: true
        cancelled_by:
          type: string
          nullable: true
          description: >-
            Username (email) of the staff user who cancelled the appointment.


            Sourced from the annotation on the list queryset
            (``_cancelled_by_email``

            subquery in :meth:`AppointmentListView.get_queryset`).  Returns
            ``None``

            for non-cancelled rows, for appointments that were cancelled before
            the

            status-log was introduced, or when the detail endpoint is used (the

            annotation is list-only).
          readOnly: true
        cancelled_by_name:
          type: string
          nullable: true
          description: |-
            Display name of the staff user who cancelled the appointment.

            Built from the first_name/last_name subquery annotations; falls back
            to the email annotation when both name fields are blank, mirroring
            :class:`AppointmentStatusLogSerializer.get_changed_by_name`.
          readOnly: true
        scheduled_procedures:
          type: array
          items:
            $ref: '#/components/schemas/AppointmentProcedure'
        custom_data: {}
        allow_overbooking:
          type: boolean
          default: false
        skip_validation:
          type: boolean
          default: false
      required:
        - cancelled_by
        - cancelled_by_name
        - id
        - patient_name
        - provider_hygienist_name
        - provider_name
        - status
    AppointmentUpdateRequest:
      type: object
      description: >-
        Staff PUT/PATCH serializer — pins ``status`` read-only.


        Staff UIs must route status changes through the ``/transition/``
        endpoint,

        which enforces :data:`apps.scheduling.services.ALLOWED_TRANSITIONS` and

        writes an :class:`AppointmentStatusLog` row.  This serializer removes

        ``status`` from the writable field set so a stray ``PATCH {"status":
        ...}``

        from the web app is silently ignored rather than bypassing the state

        machine.


        The bulk-import path intentionally uses

        :class:`AppointmentBulkWriteSerializer` instead — the source PMS is

        authoritative for status and must be able to write it directly.
      properties:
        patient:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        provider_hygienist:
          type: string
          format: uuid
          nullable: true
        operatory:
          type: string
          format: uuid
          nullable: true
        treatment_plan:
          type: string
          format: uuid
          nullable: true
          description: Treatment plan this appointment was scheduled from
        date_time:
          type: string
          format: date-time
          nullable: true
        duration_minutes:
          type: integer
          maximum: 2147483647
          minimum: 0
        confirmed_status:
          oneOf:
            - $ref: '#/components/schemas/ConfirmedStatusEnum'
            - $ref: '#/components/schemas/BlankEnum'
        appointment_type:
          type: string
          description: e.g. New Patient Exam, Crown Prep
          maxLength: 100
        is_hygiene:
          type: boolean
        is_new_patient:
          type: boolean
        procedure_description:
          type: string
        note:
          type: string
        cancellation_reason:
          type: string
          description: Patient-supplied reason for cancellation
        reschedule_requested:
          type: boolean
          description: Patient asked the front desk to reschedule
        arrived_at:
          type: string
          format: date-time
          nullable: true
        seated_at:
          type: string
          format: date-time
          nullable: true
        ready_for_doctor_at:
          type: string
          format: date-time
          nullable: true
          description: When hygienist signaled ready for doctor exam
        ready_for_walkout_at:
          type: string
          format: date-time
          nullable: true
          description: When patient is ready for walkout (checkout/scheduling)
        dismissed_at:
          type: string
          format: date-time
          nullable: true
        dollars_scheduled:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Scheduled procedure total from the source PMS; nullable when unknown
        scheduled_procedures:
          type: array
          items:
            $ref: '#/components/schemas/AppointmentProcedureRequest'
        custom_data: {}
        allow_overbooking:
          type: boolean
          default: false
        skip_validation:
          type: boolean
          default: false
    AutoNote:
      type: object
      description: |-
        Read/write serializer for :class:`AutoNote` chairside macros.

        The ``prompts`` field is a JSON-typed list of operator-fill widget
        specs validated by :func:`_validate_autonote_prompts`.  See the model
        docstring for the canonical contract.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          description: Operator-visible macro label, e.g. 'Perio Max'.
          maxLength: 128
        body:
          type: string
          description: >-
            Template body with single-braced {key} placeholders matched against
            prompts[].key.
        category:
          type: string
          description: >-
            Free-form grouping (e.g. 'Hygiene', 'Endo').  PMS-supplied; not
            enumerated.
          maxLength: 64
        prompts:
          type: array
          items:
            type: object
            additionalProperties: {}
          description: Operator-fill widget specs.  See AutoNote model docs for shape.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - body
        - created
        - id
        - modified
        - name
    AutoNoteRequest:
      type: object
      description: |-
        Read/write serializer for :class:`AutoNote` chairside macros.

        The ``prompts`` field is a JSON-typed list of operator-fill widget
        specs validated by :func:`_validate_autonote_prompts`.  See the model
        docstring for the canonical contract.
      properties:
        name:
          type: string
          minLength: 1
          description: Operator-visible macro label, e.g. 'Perio Max'.
          maxLength: 128
        body:
          type: string
          minLength: 1
          description: >-
            Template body with single-braced {key} placeholders matched against
            prompts[].key.
        category:
          type: string
          description: >-
            Free-form grouping (e.g. 'Hygiene', 'Endo').  PMS-supplied; not
            enumerated.
          maxLength: 64
        prompts:
          type: array
          items:
            type: object
            additionalProperties: {}
          description: Operator-fill widget specs.  See AutoNote model docs for shape.
        custom_data: {}
      required:
        - body
        - name
    BalanceSummary:
      type: object
      description: Computed balance summary returned by the balance-summary endpoint.
      properties:
        total_charges:
          type: string
          format: decimal
          pattern: ^-?\d{0,10}(?:\.\d{0,2})?$
        total_payments:
          type: string
          format: decimal
          pattern: ^-?\d{0,10}(?:\.\d{0,2})?$
        total_adjustments:
          type: string
          format: decimal
          pattern: ^-?\d{0,10}(?:\.\d{0,2})?$
        insurance_paid:
          type: string
          format: decimal
          pattern: ^-?\d{0,10}(?:\.\d{0,2})?$
        insurance_estimate:
          type: string
          format: decimal
          pattern: ^-?\d{0,10}(?:\.\d{0,2})?$
        balance:
          type: string
          format: decimal
          pattern: ^-?\d{0,10}(?:\.\d{0,2})?$
      required:
        - balance
        - insurance_estimate
        - insurance_paid
        - total_adjustments
        - total_charges
        - total_payments
    Banner:
      type: object
      description: |-
        Full CRUD payload for a Banner.

        Exactly one of ``client`` / ``practice`` must be supplied.  This is
        enforced both by the model-level CheckConstraint and by ``validate()``
        here so the API returns a 400 with a useful message instead of an
        IntegrityError.

        The ``starts_at`` / ``expires_at`` fields must be timezone-aware UTC
        datetimes per the project-wide convention; DRF's default DateTimeField
        handles parsing/output in ISO-8601 with timezone offsets.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        title:
          type: string
          description: Optional short heading shown above the message.
          maxLength: 120
        message:
          type: string
          description: Plain-text banner body.  HTML is not rendered by the client.
        severity:
          allOf:
            - $ref: '#/components/schemas/SeverityEnum'
          description: |-
            Visual treatment hint for the frontend (info/warning/urgent).

            * `info` - Info
            * `warning` - Warning
            * `urgent` - Urgent
        starts_at:
          type: string
          format: date-time
          description: UTC datetime the banner becomes active.  Defaults to now.
        expires_at:
          type: string
          format: date-time
          description: UTC datetime after which the banner is no longer active.
        client:
          type: string
          format: uuid
          nullable: true
          description: Client this banner targets.  Mutually exclusive with `practice`.
        practice:
          type: string
          format: uuid
          nullable: true
          description: Practice this banner targets.  Mutually exclusive with `client`.
        is_active:
          type: boolean
          description: Return True if the banner is currently within its active window.
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - expires_at
        - id
        - is_active
        - message
        - modified
    BannerRequest:
      type: object
      description: |-
        Full CRUD payload for a Banner.

        Exactly one of ``client`` / ``practice`` must be supplied.  This is
        enforced both by the model-level CheckConstraint and by ``validate()``
        here so the API returns a 400 with a useful message instead of an
        IntegrityError.

        The ``starts_at`` / ``expires_at`` fields must be timezone-aware UTC
        datetimes per the project-wide convention; DRF's default DateTimeField
        handles parsing/output in ISO-8601 with timezone offsets.
      properties:
        title:
          type: string
          description: Optional short heading shown above the message.
          maxLength: 120
        message:
          type: string
          minLength: 1
          description: Plain-text banner body.  HTML is not rendered by the client.
        severity:
          allOf:
            - $ref: '#/components/schemas/SeverityEnum'
          description: |-
            Visual treatment hint for the frontend (info/warning/urgent).

            * `info` - Info
            * `warning` - Warning
            * `urgent` - Urgent
        starts_at:
          type: string
          format: date-time
          description: UTC datetime the banner becomes active.  Defaults to now.
        expires_at:
          type: string
          format: date-time
          description: UTC datetime after which the banner is no longer active.
        client:
          type: string
          format: uuid
          nullable: true
          description: Client this banner targets.  Mutually exclusive with `practice`.
        practice:
          type: string
          format: uuid
          nullable: true
          description: Practice this banner targets.  Mutually exclusive with `client`.
      required:
        - expires_at
        - message
    BenefitTypeEnum:
      enum:
        - percentage
        - deductible
        - limitation
        - exclusion
        - max
        - waiting_period
      type: string
      description: |-
        * `percentage` - Coinsurance Percentage
        * `deductible` - Deductible
        * `limitation` - Frequency Limitation
        * `exclusion` - Exclusion
        * `max` - Annual Maximum
        * `waiting_period` - Waiting Period
    BillingPortalSessionError:
      type: object
      description: Shape of the 422 / 502 / 503 envelopes for OpenAPI.
      properties:
        detail:
          type: string
        code:
          type: string
      required:
        - detail
    BillingPortalSessionRequestRequest:
      type: object
      description: |-
        Optional payload for the portal-session endpoint.

        The frontend may pass a ``return_url`` to override the configured
        default (handy for deep-linking the customer back to a specific page
        after they finish managing billing).  When omitted we fall back to
        ``settings.STRIPE_PORTAL_RETURN_URL``.
      properties:
        return_url:
          type: string
          format: uri
          minLength: 1
    BillingPortalSessionResponse:
      type: object
      description: >-
        Shape of the 200 response: a single ``url`` string for the SPA to
        redirect to.
      properties:
        url:
          type: string
          format: uri
      required:
        - url
    BlankEnum:
      enum:
        - ''
    CancellationReportResponse:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/CancellationReportRow'
      required:
        - results
    CancellationReportRow:
      type: object
      properties:
        id:
          type: string
        cancelled_at:
          type: string
          format: date-time
          nullable: true
        scheduled_start:
          type: string
          format: date-time
          nullable: true
        lead_time_hours:
          type: string
          nullable: true
        patient_id:
          type: string
          nullable: true
        patient_name:
          type: string
          nullable: true
        provider_name:
          type: string
          nullable: true
        operatory_name:
          type: string
          nullable: true
        procedure_description:
          type: string
          nullable: true
        cancel_reason:
          type: string
          nullable: true
        cancelled_by_id:
          type: string
          nullable: true
        cancelled_by_name:
          type: string
          nullable: true
        rebooked:
          type: boolean
        rebook_appointment_id:
          type: string
          nullable: true
        rebook_date:
          type: string
          format: date
          nullable: true
        rebook_lag_days:
          type: integer
          nullable: true
        scheduled_production:
          type: string
      required:
        - cancel_reason
        - cancelled_at
        - cancelled_by_id
        - cancelled_by_name
        - id
        - lead_time_hours
        - operatory_name
        - patient_id
        - patient_name
        - procedure_description
        - provider_name
        - rebook_appointment_id
        - rebook_date
        - rebook_lag_days
        - rebooked
        - scheduled_production
        - scheduled_start
    CaptureSessionStatus:
      type: object
      description: Read-only session status for crash recovery.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        status:
          allOf:
            - $ref: '#/components/schemas/CaptureSessionStatusEnum'
          readOnly: true
        exam:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Created during token exchange
        template:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        template_name:
          type: string
          readOnly: true
        workstation_name:
          type: string
          readOnly: true
          description: Computer name from the capture workstation
        image_count:
          type: string
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - exam
        - id
        - image_count
        - status
        - template
        - template_name
        - workstation_name
    CaptureSessionStatusEnum:
      enum:
        - pending
        - active
        - completed
        - expired
      type: string
      description: |-
        * `pending` - Pending
        * `active` - Active
        * `completed` - Completed
        * `expired` - Expired
    CaseAcceptancePracticeRow:
      type: object
      description: Per-practice case-acceptance row.
      properties:
        practice_id:
          type: string
        practice_name:
          type: string
        presented_amount:
          type: string
        accepted_amount:
          type: string
        acceptance_rate_pct:
          type: string
        same_visit_accepted_pct:
          type: string
        delayed_accepted_pct:
          type: string
      required:
        - acceptance_rate_pct
        - accepted_amount
        - delayed_accepted_pct
        - practice_id
        - practice_name
        - presented_amount
        - same_visit_accepted_pct
    CaseAcceptanceResponse:
      type: object
      description: Response for ``/reports/case-acceptance/``.
      properties:
        date_from:
          type: string
        date_to:
          type: string
        totals:
          $ref: '#/components/schemas/_CaseAcceptanceTotals'
        practices:
          type: array
          items:
            $ref: '#/components/schemas/CaseAcceptancePracticeRow'
      required:
        - date_from
        - date_to
        - practices
        - totals
    CategoryEnum:
      enum:
        - exam
        - hygiene
        - endo
        - oral_surgery
        - general
      type: string
      description: |-
        * `exam` - Exam
        * `hygiene` - Hygiene
        * `endo` - Endodontics
        * `oral_surgery` - Oral Surgery
        * `general` - General
    ChainCappedTokenRefresh:
      type: object
      description: >-
        Refresh-token serializer that enforces the chain-absolute-lifetime cap.


        Wraps
        :class:`rest_framework_simplejwt.serializers.TokenRefreshSerializer`

        with two additional behaviours:


        * Before SimpleJWT rotates the token, check whether the chain has
          lived past ``settings.REFRESH_CHAIN_ABSOLUTE_LIFETIME_SECONDS``.
          If so, raise :class:`TokenError` so the framework converts the
          rejection into the standard ``invalid_grant`` 401 response (the
          same error shape clients already handle for expired tokens).
        * After rotation, copy the original ``chain_started_at`` value into
          the new refresh token so the cap remains anchored to the original
          chain start, *not* to the most recent rotation.

        The class-level ``token_class`` is set to

        :class:`ChainCappedRefreshToken` so the serializer's

        ``self.token_class(...)`` parse keeps the typed subclass throughout

        rotation, including the ``str(refresh)`` re-encoding at the end.
      properties:
        refresh:
          type: string
        access:
          type: string
          readOnly: true
      required:
        - access
        - refresh
    ChainCappedTokenRefreshRequest:
      type: object
      description: >-
        Refresh-token serializer that enforces the chain-absolute-lifetime cap.


        Wraps
        :class:`rest_framework_simplejwt.serializers.TokenRefreshSerializer`

        with two additional behaviours:


        * Before SimpleJWT rotates the token, check whether the chain has
          lived past ``settings.REFRESH_CHAIN_ABSOLUTE_LIFETIME_SECONDS``.
          If so, raise :class:`TokenError` so the framework converts the
          rejection into the standard ``invalid_grant`` 401 response (the
          same error shape clients already handle for expired tokens).
        * After rotation, copy the original ``chain_started_at`` value into
          the new refresh token so the cap remains anchored to the original
          chain start, *not* to the most recent rotation.

        The class-level ``token_class`` is set to

        :class:`ChainCappedRefreshToken` so the serializer's

        ``self.token_class(...)`` parse keeps the typed subclass throughout

        rotation, including the ``str(refresh)`` re-encoding at the end.
      properties:
        refresh:
          type: string
          minLength: 1
      required:
        - refresh
    ChannelEnum:
      enum:
        - email
        - sms
        - kiosk
      type: string
      description: |-
        * `email` - Email
        * `sms` - SMS
        * `kiosk` - In-office kiosk
    CheckoutBalanceSummary:
      type: object
      description: Balance block included in the checkout response.
      properties:
        total_charges:
          type: string
        total_payments:
          type: string
        total_adjustments:
          type: string
        insurance_paid:
          type: string
        balance:
          type: string
      required:
        - balance
        - insurance_paid
        - total_adjustments
        - total_charges
        - total_payments
    CheckoutPaymentRequest:
      type: object
      description: Optional payment collected at checkout.
      properties:
        amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        type:
          type: string
          minLength: 1
          default: ''
          maxLength: 100
        note:
          type: string
          minLength: 1
          default: ''
      required:
        - amount
    CheckoutProcedureRequest:
      type: object
      description: A single procedure within a checkout request.
      properties:
        proc_code:
          type: string
          minLength: 1
          maxLength: 20
        description:
          type: string
          default: ''
          maxLength: 255
        fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        tooth_num:
          type: string
          default: ''
          maxLength: 10
        surface:
          type: string
          default: ''
          maxLength: 20
        provider:
          type: string
          format: uuid
          nullable: true
      required:
        - fee
        - proc_code
    CheckoutRequest:
      type: object
      description: Top-level checkout/walkout serializer.
      properties:
        appointment_id:
          type: string
          format: uuid
        procedures:
          type: array
          items:
            $ref: '#/components/schemas/CheckoutProcedureRequest'
        payment:
          allOf:
            - $ref: '#/components/schemas/CheckoutPaymentRequest'
          nullable: true
        create_claim:
          type: boolean
          default: true
      required:
        - appointment_id
        - procedures
    CheckoutResponse:
      type: object
      description: Response from ``POST /patient/<id>/billing/checkout/``.
      properties:
        procedures_count:
          type: integer
        payment_amount:
          type: string
        claim_id:
          type: string
          nullable: true
        balance_summary:
          $ref: '#/components/schemas/CheckoutBalanceSummary'
      required:
        - balance_summary
        - claim_id
        - payment_amount
        - procedures_count
    ClaimProcedure:
      type: object
      description: |-
        Read-only serializer for claim procedure lines.

        Exposes both the legacy ``proc_code`` string and a nested
        ``procedure_code`` object when the FK is resolved.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        custom_data:
          readOnly: true
        procedure_code:
          allOf:
            - $ref: '#/components/schemas/ProcedureCodeNested'
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        proc_code:
          type: string
          readOnly: true
          maxLength: 20
        fee_billed:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        ins_pay_est:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        ins_pay_amt:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        ded_applied:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        write_off:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        status:
          type: string
          readOnly: true
          maxLength: 50
        proc_date:
          type: string
          format: date
          readOnly: true
          nullable: true
        created_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        modified_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        practice:
          type: string
          format: uuid
          readOnly: true
        claim:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        procedure:
          type: string
          format: uuid
          readOnly: true
          nullable: true
      required:
        - claim
        - created
        - created_by
        - custom_data
        - ded_applied
        - fee_billed
        - id
        - ins_pay_amt
        - ins_pay_est
        - modified
        - modified_by
        - patient
        - practice
        - proc_code
        - proc_date
        - procedure
        - procedure_code
        - status
        - write_off
    ClaimProcedureWrite:
      type: object
      description: >-
        Write serializer for claim procedure lines (used in nested claim
        creation).


        Mounted as a nested serializer inside

        :class:`InsuranceClaimWriteSerializer`: the parent claim provides

        ``claim``, ``practice``, and ``patient`` server-side.  For the

        standalone bulk-import path used by the converters

        (``/api/v1/practice/<uid>/bulk/claim-procedure/``), see

        :class:`ClaimProcedureBulkWriteSerializer`.
      properties:
        procedure:
          type: string
          format: uuid
          nullable: true
        proc_code:
          type: string
          maxLength: 20
        procedure_code:
          type: string
          format: uuid
          nullable: true
        fee_billed:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        ins_pay_est:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        ded_applied:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        write_off:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        custom_data: {}
    ClaimProcedureWriteRequest:
      type: object
      description: >-
        Write serializer for claim procedure lines (used in nested claim
        creation).


        Mounted as a nested serializer inside

        :class:`InsuranceClaimWriteSerializer`: the parent claim provides

        ``claim``, ``practice``, and ``patient`` server-side.  For the

        standalone bulk-import path used by the converters

        (``/api/v1/practice/<uid>/bulk/claim-procedure/``), see

        :class:`ClaimProcedureBulkWriteSerializer`.
      properties:
        procedure:
          type: string
          format: uuid
          nullable: true
        proc_code:
          type: string
          maxLength: 20
        procedure_code:
          type: string
          format: uuid
          nullable: true
        fee_billed:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        ins_pay_est:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        ded_applied:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        write_off:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        custom_data: {}
    ClaimStatusEnum:
      enum:
        - draft
        - submitted
        - accepted
        - rejected
        - resubmitted
        - paid
        - partial
        - denied
        - appealed
        - void
        - imported
      type: string
      description: |-
        * `draft` - Draft
        * `submitted` - Submitted
        * `accepted` - Accepted
        * `rejected` - Rejected
        * `resubmitted` - Resubmitted
        * `paid` - Paid
        * `partial` - Partially Paid
        * `denied` - Denied
        * `appealed` - Appealed
        * `void` - Void
        * `imported` - Imported
    ClaimStatusLog:
      type: object
      description: Read-only serializer for claim status transition audit log.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        claim:
          type: string
          format: uuid
          readOnly: true
        from_status:
          type: string
          readOnly: true
          maxLength: 20
        to_status:
          type: string
          readOnly: true
          maxLength: 20
        changed_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        changed_by_email:
          type: string
          readOnly: true
        changed_at:
          type: string
          format: date-time
          readOnly: true
        note:
          type: string
          readOnly: true
        custom_data:
          readOnly: true
      required:
        - changed_at
        - changed_by
        - changed_by_email
        - claim
        - custom_data
        - from_status
        - id
        - note
        - to_status
    ClaimTransition:
      type: object
      description: |-
        Serializer for claim status transitions.

        Validates that the requested ``new_status`` is a legal transition
        from the claim's current status.
      properties:
        new_status:
          $ref: '#/components/schemas/ClaimStatusEnum'
        note:
          type: string
          default: ''
      required:
        - new_status
    ClaimTransitionRequest:
      type: object
      description: |-
        Serializer for claim status transitions.

        Validates that the requested ``new_status`` is a legal transition
        from the claim's current status.
      properties:
        new_status:
          $ref: '#/components/schemas/ClaimStatusEnum'
        note:
          type: string
          default: ''
      required:
        - new_status
    Client:
      type: object
      description: Lightweight representation of a client (organization).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          maxLength: 255
        logo_url:
          type: string
          readOnly: true
        accent_color:
          type: string
          readOnly: true
          description: 'Brand accent colour as a 7-character #RRGGBB hex string.'
        scheduling_enabled:
          type: boolean
          description: Enable scheduling module
        clinical_enabled:
          type: boolean
          description: Enable clinical module
        imaging_enabled:
          type: boolean
          description: Enable imaging module
        insurance_enabled:
          type: boolean
          description: Enable insurance module
        billing_enabled:
          type: boolean
          description: Enable billing module
        doccenter_enabled:
          type: boolean
          description: Enable document center module
        portal_enabled:
          type: boolean
          description: Enable patient portal module
        sync_enabled:
          type: boolean
          description: Enable sync/import module
        max_session_hours:
          type: integer
          maximum: 32767
          minimum: 0
          description: >-
            Maximum login session duration in hours. Users must re-authenticate
            after this time.
        is_active:
          type: boolean
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
        active_banners:
          type: string
          readOnly: true
      required:
        - accent_color
        - active_banners
        - created
        - id
        - is_active
        - logo_url
        - modified
        - name
    ClientLogoUrlResponse:
      type: object
      properties:
        logo_url:
          type: string
          format: uri
      required:
        - logo_url
    ClientMembership:
      type: object
      description: >-
        Serializer for a user's direct client-level memberships (nested in
        UserSerializer).


        Mirrors ``PracticeMembershipSerializer`` but exposes Client-scoped roles

        (OWNER / BILLING / TECHNICAL) that grant access across every practice

        owned by the client, independent of any per-practice membership.
      properties:
        client_id:
          type: string
          format: uuid
        client_name:
          type: string
        role:
          $ref: '#/components/schemas/ClientMembershipRoleEnum'
        is_active:
          type: boolean
      required:
        - client_id
        - client_name
        - role
    ClientMembershipRequest:
      type: object
      description: >-
        Serializer for a user's direct client-level memberships (nested in
        UserSerializer).


        Mirrors ``PracticeMembershipSerializer`` but exposes Client-scoped roles

        (OWNER / BILLING / TECHNICAL) that grant access across every practice

        owned by the client, independent of any per-practice membership.
      properties:
        client_id:
          type: string
          format: uuid
        client_name:
          type: string
          minLength: 1
        role:
          $ref: '#/components/schemas/ClientMembershipRoleEnum'
        is_active:
          type: boolean
      required:
        - client_id
        - client_name
        - role
    ClientMembershipRoleEnum:
      enum:
        - OWNER
        - BILLING
        - TECHNICAL
      type: string
      description: |-
        * `OWNER` - Owner
        * `BILLING` - Billing
        * `TECHNICAL` - Technical
    ClinicalNote:
      type: object
      description: >-
        Read serializer for clinical notes with denormalized provider/template
        names.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        appointment:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        provider:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        provider_name:
          type: string
          nullable: true
          description: Return the provider's display name, or None if no provider is set.
          readOnly: true
        template:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: >-
            Template used to create this note (reference only, content is
            copied)
        template_name:
          type: string
          nullable: true
          description: Return the template name, or None if no template is set.
          readOnly: true
        note_type:
          allOf:
            - $ref: '#/components/schemas/NoteTypeEnum'
          readOnly: true
        subject:
          type: string
          readOnly: true
        content:
          type: string
          readOnly: true
          description: Plain text or structured note content
        note_date:
          type: string
          format: date
          readOnly: true
          nullable: true
        is_signed:
          type: boolean
          readOnly: true
        signed_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        signed_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        is_addendum:
          type: boolean
          readOnly: true
          description: Addendum to a previous note
        parent_note:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: If is_addendum=True, the original note this amends
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        created_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - appointment
        - content
        - created
        - created_by
        - id
        - is_addendum
        - is_signed
        - modified
        - note_date
        - note_type
        - parent_note
        - patient
        - provider
        - provider_name
        - signed_at
        - signed_by
        - subject
        - template
        - template_name
    ClinicalNoteWrite:
      type: object
      description: |-
        Write serializer for creating and updating clinical notes.

        Enforces HIPAA immutability: signed notes cannot be modified.
        The ``patient`` field is excluded because it is set from the URL path
        by the view's ``perform_create`` method.

        GH-711: when ``make_bulk_serializer`` injects ``patient``, the FK plus
        ``appointment`` / ``provider`` / ``template`` / ``parent_note`` are all
        validated against the URL practice.
      properties:
        appointment:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        template:
          type: string
          format: uuid
          nullable: true
          description: >-
            Template used to create this note (reference only, content is
            copied)
        note_type:
          $ref: '#/components/schemas/NoteTypeEnum'
        subject:
          type: string
          maxLength: 200
        content:
          type: string
          description: Plain text or structured note content
        note_date:
          type: string
          format: date
          nullable: true
        is_addendum:
          type: boolean
          description: Addendum to a previous note
        parent_note:
          type: string
          format: uuid
          nullable: true
          description: If is_addendum=True, the original note this amends
        custom_data: {}
    ClinicalNoteWriteRequest:
      type: object
      description: |-
        Write serializer for creating and updating clinical notes.

        Enforces HIPAA immutability: signed notes cannot be modified.
        The ``patient`` field is excluded because it is set from the URL path
        by the view's ``perform_create`` method.

        GH-711: when ``make_bulk_serializer`` injects ``patient``, the FK plus
        ``appointment`` / ``provider`` / ``template`` / ``parent_note`` are all
        validated against the URL practice.
      properties:
        appointment:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        template:
          type: string
          format: uuid
          nullable: true
          description: >-
            Template used to create this note (reference only, content is
            copied)
        note_type:
          $ref: '#/components/schemas/NoteTypeEnum'
        subject:
          type: string
          maxLength: 200
        content:
          type: string
          description: Plain text or structured note content
        note_date:
          type: string
          format: date
          nullable: true
        is_addendum:
          type: boolean
          description: Addendum to a previous note
        parent_note:
          type: string
          format: uuid
          nullable: true
          description: If is_addendum=True, the original note this amends
        custom_data: {}
    CommLog:
      type: object
      description: |-
        Read serializer for communication log entries.

        Returns all fields including patient FK and timestamps.
        ``custom_data`` is scoped per OAuth application.

        Surfaces two PHI-aware fields added in GH-575:

        * ``is_imported`` -- read-only flag.  ``True`` means the row was
          brought across by a sync tool (escape-opendental, escape-eaglesoft,
          ...) and pre-dates Escape's "no PHI in email/SMS" policy.  Clients
          MUST suppress any "resend" / "use as template" affordance on these
          rows.  Native rows (``is_imported=False``) are safe to template.
        * ``phi_redacted_note`` -- computed.  The ``note`` body with the PHI
          keyword list (diagnosis, prescription, tooth, crown, ...) replaced
          by ``[REDACTED]``.  Surfaces that have no business showing PHI
          (search snippets, dashboard widgets, notification toasts) consume
          this instead of ``note``.  The raw ``note`` is still returned for
          portal display where PHI is appropriate.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          nullable: true
        date_time:
          type: string
          format: date-time
          nullable: true
        mode:
          $ref: '#/components/schemas/ModeEnum'
        direction:
          $ref: '#/components/schemas/Direction80cEnum'
        outcome:
          allOf:
            - $ref: '#/components/schemas/OutcomeEnum'
          description: >-
            What happened on the contact attempt.  Only ``success`` rows count
            toward the recall-list ``last_reached_at`` annotation.


            * `success` - Success

            * `no_answer` - No Answer

            * `voicemail` - Voicemail

            * `bounced` - Bounced

            * `bad_number` - Bad Number

            * `other` - Other
        subject:
          type: string
          description: Short label, e.g. 'Recall reminder', 'Treatment plan follow-up'.
          maxLength: 255
        note:
          type: string
        phi_redacted_note:
          type: string
          description: >-
            Computed read-only field: ``note`` with PHI keywords scrubbed
            (replaced by ``[REDACTED]``).  Use this in non-portal surfaces
            (search results, dashboard previews) where PHI must not be
            displayed.  The keyword list is defined in
            ``apps.comms.redaction.PHI_KEYWORDS``.
          readOnly: true
        staff_user:
          type: string
          format: uuid
          nullable: true
          description: >-
            Staff member who logged the contact.  Null for system-generated
            entries.
        recall:
          type: string
          format: uuid
          nullable: true
          description: Optional link to the specific recall this contact addresses.
        is_imported:
          type: boolean
          readOnly: true
          description: >-
            True if this row was imported from a legacy PMS (escape-opendental,
            escape-eaglesoft, ...) rather than created natively in Escape. 
            Imported rows may carry PHI in the ``note`` body that pre-dates the
            Escape BAA / no-PHI-in-comms policy (GH-575); clients must NOT
            expose 'resend' or 'use as template' affordances on imported rows,
            and any future send endpoint that takes a
            ``replicate_of_commlog_id`` MUST refuse imported sources.
        custom_data: {}
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - id
        - is_imported
        - modified
        - phi_redacted_note
    CommLogBulkWrite:
      type: object
      description: >-
        Write serializer for practice-wide bulk commlog import.


        Includes ``patient`` (required) since the endpoint is not
        patient-scoped.

        Used by :class:`PracticeCommLogListCreateView` for POST/PATCH arrays
        from

        sync tools that iterate practice-wide instead of per-patient.


        Accepts ``is_imported`` so escape-opendental / escape-eaglesoft can

        flag every row they ingest as imported (GH-575).  The model default

        is ``False`` for safety -- if the sync tool forgets to pass the flag,

        the row falls through as a native entry, which is the wrong failure

        direction but at least preserves the historical behaviour while the

        sync tools catch up.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          nullable: true
        date_time:
          type: string
          format: date-time
          nullable: true
        mode:
          $ref: '#/components/schemas/ModeEnum'
        direction:
          $ref: '#/components/schemas/Direction80cEnum'
        outcome:
          allOf:
            - $ref: '#/components/schemas/OutcomeEnum'
          description: >-
            What happened on the contact attempt.  Only ``success`` rows count
            toward the recall-list ``last_reached_at`` annotation.


            * `success` - Success

            * `no_answer` - No Answer

            * `voicemail` - Voicemail

            * `bounced` - Bounced

            * `bad_number` - Bad Number

            * `other` - Other
        subject:
          type: string
          description: Short label, e.g. 'Recall reminder', 'Treatment plan follow-up'.
          maxLength: 255
        note:
          type: string
        staff_user:
          type: string
          format: uuid
          nullable: true
          description: >-
            Staff member who logged the contact.  Null for system-generated
            entries.
        recall:
          type: string
          format: uuid
          nullable: true
          description: Optional link to the specific recall this contact addresses.
        is_imported:
          type: boolean
          description: >-
            True if this row was imported from a legacy PMS (escape-opendental,
            escape-eaglesoft, ...) rather than created natively in Escape. 
            Imported rows may carry PHI in the ``note`` body that pre-dates the
            Escape BAA / no-PHI-in-comms policy (GH-575); clients must NOT
            expose 'resend' or 'use as template' affordances on imported rows,
            and any future send endpoint that takes a
            ``replicate_of_commlog_id`` MUST refuse imported sources.
        custom_data: {}
      required:
        - id
    CommLogBulkWriteRequest:
      type: object
      description: >-
        Write serializer for practice-wide bulk commlog import.


        Includes ``patient`` (required) since the endpoint is not
        patient-scoped.

        Used by :class:`PracticeCommLogListCreateView` for POST/PATCH arrays
        from

        sync tools that iterate practice-wide instead of per-patient.


        Accepts ``is_imported`` so escape-opendental / escape-eaglesoft can

        flag every row they ingest as imported (GH-575).  The model default

        is ``False`` for safety -- if the sync tool forgets to pass the flag,

        the row falls through as a native entry, which is the wrong failure

        direction but at least preserves the historical behaviour while the

        sync tools catch up.
      properties:
        patient:
          type: string
          format: uuid
          nullable: true
        date_time:
          type: string
          format: date-time
          nullable: true
        mode:
          $ref: '#/components/schemas/ModeEnum'
        direction:
          $ref: '#/components/schemas/Direction80cEnum'
        outcome:
          allOf:
            - $ref: '#/components/schemas/OutcomeEnum'
          description: >-
            What happened on the contact attempt.  Only ``success`` rows count
            toward the recall-list ``last_reached_at`` annotation.


            * `success` - Success

            * `no_answer` - No Answer

            * `voicemail` - Voicemail

            * `bounced` - Bounced

            * `bad_number` - Bad Number

            * `other` - Other
        subject:
          type: string
          description: Short label, e.g. 'Recall reminder', 'Treatment plan follow-up'.
          maxLength: 255
        note:
          type: string
        staff_user:
          type: string
          format: uuid
          nullable: true
          description: >-
            Staff member who logged the contact.  Null for system-generated
            entries.
        recall:
          type: string
          format: uuid
          nullable: true
          description: Optional link to the specific recall this contact addresses.
        is_imported:
          type: boolean
          description: >-
            True if this row was imported from a legacy PMS (escape-opendental,
            escape-eaglesoft, ...) rather than created natively in Escape. 
            Imported rows may carry PHI in the ``note`` body that pre-dates the
            Escape BAA / no-PHI-in-comms policy (GH-575); clients must NOT
            expose 'resend' or 'use as template' affordances on imported rows,
            and any future send endpoint that takes a
            ``replicate_of_commlog_id`` MUST refuse imported sources.
        custom_data: {}
    CommLogWrite:
      type: object
      description: |-
        Write serializer for creating and updating communication log entries.

        Practice and patient are set from the URL path by the view.

        ``is_imported`` is deliberately NOT writable here -- the patient-
        scoped create endpoint is for native Escape entries, which are
        always ``is_imported=False`` (the model default).  The bulk
        import path uses :class:`CommLogBulkWriteSerializer`, which DOES
        accept ``is_imported`` from sync tools.

        GH-711: ``patient`` (injected on the bulk path) and ``recall`` are
        practice-validated.  ``staff_user`` points at the global User table
        (not practice-scoped) so it is intentionally excluded — the existing
        membership check on user-action endpoints handles its access control.
      properties:
        date_time:
          type: string
          format: date-time
          nullable: true
        mode:
          $ref: '#/components/schemas/ModeEnum'
        direction:
          $ref: '#/components/schemas/Direction80cEnum'
        outcome:
          allOf:
            - $ref: '#/components/schemas/OutcomeEnum'
          description: >-
            What happened on the contact attempt.  Only ``success`` rows count
            toward the recall-list ``last_reached_at`` annotation.


            * `success` - Success

            * `no_answer` - No Answer

            * `voicemail` - Voicemail

            * `bounced` - Bounced

            * `bad_number` - Bad Number

            * `other` - Other
        subject:
          type: string
          description: Short label, e.g. 'Recall reminder', 'Treatment plan follow-up'.
          maxLength: 255
        note:
          type: string
        staff_user:
          type: string
          format: uuid
          nullable: true
          description: >-
            Staff member who logged the contact.  Null for system-generated
            entries.
        recall:
          type: string
          format: uuid
          nullable: true
          description: Optional link to the specific recall this contact addresses.
        custom_data: {}
    CommLogWriteRequest:
      type: object
      description: |-
        Write serializer for creating and updating communication log entries.

        Practice and patient are set from the URL path by the view.

        ``is_imported`` is deliberately NOT writable here -- the patient-
        scoped create endpoint is for native Escape entries, which are
        always ``is_imported=False`` (the model default).  The bulk
        import path uses :class:`CommLogBulkWriteSerializer`, which DOES
        accept ``is_imported`` from sync tools.

        GH-711: ``patient`` (injected on the bulk path) and ``recall`` are
        practice-validated.  ``staff_user`` points at the global User table
        (not practice-scoped) so it is intentionally excluded — the existing
        membership check on user-action endpoints handles its access control.
      properties:
        date_time:
          type: string
          format: date-time
          nullable: true
        mode:
          $ref: '#/components/schemas/ModeEnum'
        direction:
          $ref: '#/components/schemas/Direction80cEnum'
        outcome:
          allOf:
            - $ref: '#/components/schemas/OutcomeEnum'
          description: >-
            What happened on the contact attempt.  Only ``success`` rows count
            toward the recall-list ``last_reached_at`` annotation.


            * `success` - Success

            * `no_answer` - No Answer

            * `voicemail` - Voicemail

            * `bounced` - Bounced

            * `bad_number` - Bad Number

            * `other` - Other
        subject:
          type: string
          description: Short label, e.g. 'Recall reminder', 'Treatment plan follow-up'.
          maxLength: 255
        note:
          type: string
        staff_user:
          type: string
          format: uuid
          nullable: true
          description: >-
            Staff member who logged the contact.  Null for system-generated
            entries.
        recall:
          type: string
          format: uuid
          nullable: true
          description: Optional link to the specific recall this contact addresses.
        custom_data: {}
    ConditionEnum:
      enum:
        - missing
        - implant
        - crown
        - bridge_abutment
        - bridge_pontic
        - veneer
        - root_canal
        - extraction_planned
        - impacted
        - unerupted
        - supernumerary
        - deciduous
        - denture
        - partial_denture
        - caries
        - filling
        - sealant
        - fracture
        - watch
      type: string
      description: |-
        * `missing` - Missing
        * `implant` - Implant
        * `crown` - Crown
        * `bridge_abutment` - Bridge Abutment
        * `bridge_pontic` - Bridge Pontic
        * `veneer` - Veneer
        * `root_canal` - Root Canal
        * `extraction_planned` - Extraction Planned
        * `impacted` - Impacted
        * `unerupted` - Unerupted
        * `supernumerary` - Supernumerary
        * `deciduous` - Deciduous / Primary
        * `denture` - Denture
        * `partial_denture` - Partial Denture
        * `caries` - Caries
        * `filling` - Filling
        * `sealant` - Sealant
        * `fracture` - Fracture
        * `watch` - Watch
    ConfirmedStatusEnum:
      enum:
        - not_called
        - left_message
        - confirmed
        - econfirmed
      type: string
      description: |-
        * `not_called` - Not Called
        * `left_message` - Left Message
        * `confirmed` - Confirmed
        * `econfirmed` - eConfirmed
    ConversationDetail:
      type: object
      description: Full conversation detail including all participants.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        title:
          type: string
          readOnly: true
          description: Optional display name. Auto-generated for direct conversations.
        conversation_type:
          allOf:
            - $ref: '#/components/schemas/ConversationTypeEnum'
          readOnly: true
        operatory:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        participants:
          type: array
          items:
            $ref: '#/components/schemas/ParticipantSummary'
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - conversation_type
        - created
        - id
        - modified
        - operatory
        - participants
        - title
    ConversationList:
      type: object
      description: |-
        Conversation summary for list views.

        Includes participant names, last message preview, and unread count
        for the requesting user.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        title:
          type: string
          readOnly: true
          description: Optional display name. Auto-generated for direct conversations.
        conversation_type:
          allOf:
            - $ref: '#/components/schemas/ConversationTypeEnum'
          readOnly: true
        operatory:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        participants:
          type: array
          items:
            $ref: '#/components/schemas/ParticipantSummary'
          readOnly: true
        last_message:
          type: object
          additionalProperties: {}
          nullable: true
          description: Return the most recent message preview, if any.
          readOnly: true
        unread_count:
          type: integer
          description: Count messages created after the current user's last_read_at.
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - conversation_type
        - created
        - id
        - last_message
        - modified
        - operatory
        - participants
        - title
        - unread_count
    ConversationTypeEnum:
      enum:
        - direct
        - operatory
        - system
      type: string
      description: |-
        * `direct` - Direct
        * `operatory` - Operatory
        * `system` - System
    ConversionAcceptResponse:
      type: object
      description: Response from ``POST /sync/sessions/<id>/accept/``.
      properties:
        detail:
          type: string
        session_id:
          type: string
      required:
        - detail
        - session_id
    ConversionEntityProgress:
      type: object
      description: Per-entity conversion progress used inside session detail.
      properties:
        entity_type:
          type: string
        status:
          type: string
        total:
          type: integer
        converted:
          type: integer
        skipped:
          type: integer
        errors:
          type: integer
        error_messages:
          type: array
          items:
            type: string
      required:
        - converted
        - entity_type
        - error_messages
        - errors
        - skipped
        - status
        - total
    ConversionErrorRecord:
      type: object
      description: One row of a conversion error report.
      properties:
        entity_type:
          type: string
        external_id:
          type: string
        type:
          type: string
        error_message:
          type: string
    ConversionErrorReportResponse:
      type: object
      description: JSON response from ``GET /sync/sessions/<id>/errors/``.
      properties:
        records:
          type: array
          items:
            $ref: '#/components/schemas/ConversionErrorRecord'
      required:
        - records
    ConversionProgressSummary:
      type: object
      description: Aggregate conversion progress block.
      properties:
        entities:
          type: array
          items:
            $ref: '#/components/schemas/ConversionEntityProgress'
        total_converted:
          type: integer
        total_errors:
          type: integer
        total_skipped:
          type: integer
        is_complete:
          type: boolean
      required:
        - entities
        - is_complete
        - total_converted
        - total_errors
        - total_skipped
    ConversionRollbackResponse:
      type: object
      description: Response from ``POST /sync/sessions/<id>/rollback/``.
      properties:
        detail:
          type: string
      required:
        - detail
    CoverageCategoryEnum:
      enum:
        - diagnostic
        - preventive
        - basic
        - major
        - endodontics
        - periodontics
        - oral_surgery
        - orthodontics
        - prosthodontics
        - adjunctive
        - general
        - other
      type: string
      description: |-
        * `diagnostic` - Diagnostic
        * `preventive` - Preventive
        * `basic` - Basic
        * `major` - Major
        * `endodontics` - Endodontics
        * `periodontics` - Periodontics
        * `oral_surgery` - Oral Surgery
        * `orthodontics` - Orthodontics
        * `prosthodontics` - Prosthodontics
        * `adjunctive` - Adjunctive
        * `general` - General
        * `other` - Other
    CreateConversationRequest:
      type: object
      description: |-
        Create a new conversation with a set of participants.

        For ``direct`` conversations, ``participant_ids`` must include at
        least one other user.  The requesting user is always added
        automatically by the view.

        For ``operatory`` conversations, ``participant_ids`` is optional
        (defaults to empty) because visibility is practice-wide.  Supplying
        ``operatory`` automatically sets ``conversation_type`` to
        ``"operatory"`` so the caller doesn't need to send both.

        An optional ``body`` sends the first message in the new conversation.
      properties:
        participant_ids:
          type: array
          items:
            type: string
            format: uuid
          description: UUIDs of the users to include (the sender is added automatically).
        conversation_type:
          allOf:
            - $ref: '#/components/schemas/ConversationTypeEnum'
          default: direct
        title:
          type: string
          default: ''
          maxLength: 255
        operatory:
          type: string
          format: uuid
          nullable: true
        body:
          type: string
          default: ''
          description: Optional first message body.
    CreatePaymentIntentRequest:
      type: object
      description: |-
        Input for creating a new Stripe PaymentIntent.

        ``amount_cents`` is in the smallest currency unit (USD cents) — Stripe's
        native unit.  ``practice`` is the practice UUID the charge is being
        attributed to; required for multi-tenant audit + revenue reporting.

        Saved-card flow (api#454):

        * ``save_for_future_use`` — when true the view sets
          ``setup_future_usage="on_session"`` on the Stripe PaymentIntent so
          the card is automatically saved to the patient's Customer after a
          successful charge.  Triggers lazy-creation of the Stripe Customer
          if the patient doesn't have one yet.
        * ``payment_method`` — opaque Stripe payment-method id (``pm_...``)
          for one-click pay against a card the patient previously saved.
          The view passes it through to ``stripe.PaymentIntent.create`` and
          lets the frontend confirm it via ``confirmPayment`` without
          collecting a fresh card.
      properties:
        amount_cents:
          type: integer
          minimum: 50
          description: Amount in cents (minimum $0.50).
        description:
          type: string
          default: ''
          maxLength: 255
        practice:
          type: string
          format: uuid
          description: >-
            UUID of the practice the charge belongs to (used for metadata +
            audit trail).
        save_for_future_use:
          type: boolean
          default: false
          description: >-
            When true, attach the resulting card to the patient's Stripe
            Customer for reuse.
        payment_method:
          type: string
          default: ''
          description: Optional Stripe PaymentMethod id (``pm_...``) for one-click pay.
          maxLength: 64
      required:
        - amount_cents
    CreatePaymentIntentResponse:
      type: object
      description: Response shape for a successful PaymentIntent creation.
      properties:
        client_secret:
          type: string
          description: Pass to Stripe.js to confirm the card client-side.
        payment_intent_id:
          type: string
          description: Stripe PaymentIntent id (``pi_...``).
        audit_id:
          type: string
          format: uuid
          description: Internal PaymentIntent row id for cross-referencing audit logs.
      required:
        - audit_id
        - client_secret
        - payment_intent_id
    CsScheduleEnum:
      enum:
        - none
        - II
        - III
        - IV
        - V
      type: string
      description: |-
        * `none` - Non-Controlled
        * `II` - Schedule II
        * `III` - Schedule III
        * `IV` - Schedule IV
        * `V` - Schedule V
    DaySheetInChairPatient:
      type: object
      properties:
        patient_id:
          type: string
          format: uuid
        patient_name:
          type: string
        appointment_id:
          type: string
          format: uuid
          nullable: true
        since:
          type: string
          format: date-time
          nullable: true
      required:
        - appointment_id
        - patient_id
        - patient_name
        - since
    DaySheetOperatory:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        abbreviation:
          type: string
        is_hygiene:
          type: boolean
        provider_dentist:
          type: object
          additionalProperties: {}
          nullable: true
        provider_hygienist:
          type: object
          additionalProperties: {}
          nullable: true
        in_chair_patient:
          allOf:
            - $ref: '#/components/schemas/DaySheetInChairPatient'
          nullable: true
      required:
        - abbreviation
        - id
        - in_chair_patient
        - is_hygiene
        - name
        - provider_dentist
        - provider_hygienist
    DaySheetResponse:
      type: object
      properties:
        date:
          type: string
        operatories:
          type: array
          items:
            $ref: '#/components/schemas/DaySheetOperatory'
        blocks:
          type: array
          items:
            type: object
            additionalProperties: {}
        appointments:
          type: array
          items:
            type: object
            additionalProperties: {}
      required:
        - appointments
        - blocks
        - date
        - operatories
    DelegatedAppEntry:
      type: object
      description: |-
        One row in the Connected Apps list (api#228).

        Join-shaped payload: the underlying data lives in three tables
        (``oauth2_provider.Application``, :class:`OAuthAppProfile`, and
        :class:`OAuthAppUsageMetric`) so this is a plain ``Serializer``
        rather than a ``ModelSerializer`` -- there is no single source of
        truth to hang a ``Meta.model`` off of.
      properties:
        app_id:
          type: string
          description: OAuth2 ``Application.client_id`` -- stable public identifier.
        client_id:
          type: string
          description: >-
            Alias of ``app_id`` kept for parity with the ticket spec and to make
            the Connected Apps UI copy-paste-friendly.
        name:
          type: string
          description: Display name of the OAuth application.
        logo_url:
          type: string
          description: >-
            Best-available icon URL for the app -- uploaded logo when present,
            legacy ``logo_url`` otherwise, empty string if neither is set.
        scopes:
          type: array
          items:
            type: string
          description: >-
            Sorted list of scopes currently granted by the caller's active
            delegated-access tokens for this app.  Always includes
            ``act_on_behalf``.
        first_granted_at:
          type: string
          format: date-time
          nullable: true
          description: >-
            Earliest ``OAuthPracticeGrant.created`` across the caller's active
            ``act_on_behalf`` grants for this app.  Null when only a
            free-standing token exists without a backing grant.
        last_used_at:
          type: string
          format: date-time
          nullable: true
          description: >-
            Latest ``OAuthAppUsageMetric.bucket_start`` recorded for (this app,
            this user).  Null until the rollup worker drains the first request
            bucket, which happens within ten minutes of the first use.
      required:
        - app_id
        - client_id
        - first_granted_at
        - last_used_at
        - logo_url
        - name
        - scopes
    DelegatedAppListResponse:
      type: object
      description: |-
        Envelope for ``GET /api/v1/oauth/me/delegated-apps/``.

        The shape mirrors DRF's default paginated response (``count`` +
        ``results``) without actually paginating -- a single user is not
        going to connect thousands of third-party apps, and the web UI
        renders the list in one go.  Keeping the envelope future-proofs us
        if we ever do add pagination: the consumer won't have to change.
      properties:
        count:
          type: integer
          description: >-
            Number of rows in ``results`` (not a page total -- this endpoint is
            unpaginated).
        results:
          type: array
          items:
            $ref: '#/components/schemas/DelegatedAppEntry'
      required:
        - count
        - results
    DelegatedAppRevokeResponse:
      type: object
      description: Response body for a successful delegated-access revocation.
      properties:
        grants_revoked:
          type: integer
          description: Number of OAuthPracticeGrant rows deactivated for this user+app.
        tokens_revoked:
          type: integer
          description: >-
            Number of outstanding delegated-access tokens invalidated. Matches
            the number of ``act_on_behalf``-scoped access tokens owned by the
            signing-in user for the target app.
      required:
        - grants_revoked
        - tokens_revoked
    DentitionEnum:
      enum:
        - permanent
        - primary
      type: string
      description: |-
        * `permanent` - Permanent
        * `primary` - Primary
    DetailResponse:
      type: object
      description: 'Generic ``{"detail": "..."}`` envelope used across sync views.'
      properties:
        detail:
          type: string
      required:
        - detail
    Direction80cEnum:
      enum:
        - sent
        - received
        - neither
      type: string
      description: |-
        * `sent` - Sent
        * `received` - Received
        * `neither` - Neither
    Document:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        client:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        practice:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        category:
          type: string
          format: uuid
          nullable: true
        title:
          type: string
          maxLength: 255
        description:
          type: string
        original_filename:
          type: string
          readOnly: true
        mime_type:
          type: string
          readOnly: true
        file_size:
          type: integer
          readOnly: true
          description: File size in bytes
        page_count:
          type: integer
          readOnly: true
          nullable: true
          description: Page count for PDFs
        uploaded_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        source_application:
          type: integer
          readOnly: true
          nullable: true
          description: OAuth2 application that uploaded this document
        is_deleted:
          type: boolean
          readOnly: true
          description: Soft delete for HIPAA retention
        deleted_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        deleted_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        resolved_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: >-
            When this document was acknowledged at the practice level without
            being filed against a patient (api#511).  Used by the voicemail
            inbox triage UI: voicemails marked resolved drop out of the unlinked
            queue without being deleted.  Reversible — clearing the field
            returns the row to the queue.  Not a soft-delete: the underlying
            file is preserved and remains discoverable to the practice.
        resolved_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff member who marked the document resolved against the practice.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        download_url:
          type: string
          readOnly: true
        patient_name:
          type: string
          readOnly: true
        category_label:
          type: string
          readOnly: true
        uploaded_by_name:
          type: string
          readOnly: true
        caller_id:
          type: string
          description: >-
            E.164 caller-ID for voicemail recordings (e.g. +14155551234).  Used
            by the patient-link suggestion engine downstream of the
            voicemail-to-text pipeline.
          pattern: ^\+\d{1,15}$
          maxLength: 32
        processing_state:
          readOnly: true
          nullable: true
          description: >-
            Background-processing pipeline state for OCR / voicemail
            transcription.  Surfaces in the inbox UI via the existing
            documents_changed WebSocket event.  NULL on legacy rows that predate
            the state machine.


            * `pending` - Pending

            * `decoding` - Decoding

            * `loading_model` - Loading model

            * `processing` - Processing

            * `indexing` - Indexing

            * `complete` - Complete

            * `failed` - Failed
          oneOf:
            - $ref: '#/components/schemas/ProcessingStateEnum'
            - $ref: '#/components/schemas/NullEnum'
        processing_error:
          type: string
          readOnly: true
          description: >-
            Truncated str(exception) from the most recent failed pipeline run. 
            Plain text — never indexed.  Cleared on the next successful run.
        processing_updated_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When ``processing_state`` last changed.
        transcript:
          type: string
          readOnly: true
        transcript_confidence:
          type: string
          readOnly: true
        custom_data: {}
      required:
        - category_label
        - client
        - created
        - deleted_at
        - deleted_by
        - download_url
        - file_size
        - id
        - is_deleted
        - mime_type
        - modified
        - original_filename
        - page_count
        - patient
        - patient_name
        - practice
        - processing_error
        - processing_state
        - processing_updated_at
        - resolved_at
        - resolved_by
        - source_application
        - title
        - transcript
        - transcript_confidence
        - uploaded_by
        - uploaded_by_name
    DocumentCategory:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        slug:
          type: string
          maxLength: 50
          pattern: ^[-a-zA-Z0-9_]+$
        label:
          type: string
          maxLength: 100
        icon:
          type: string
          description: Lucide icon name
          maxLength: 50
        sort_order:
          type: integer
          maximum: 32767
          minimum: 0
        custom_data: {}
      required:
        - id
        - label
        - slug
    DocumentCategoryRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        slug:
          type: string
          minLength: 1
          maxLength: 50
          pattern: ^[-a-zA-Z0-9_]+$
        label:
          type: string
          minLength: 1
          maxLength: 100
        icon:
          type: string
          description: Lucide icon name
          maxLength: 50
        sort_order:
          type: integer
          maximum: 32767
          minimum: 0
        custom_data: {}
      required:
        - label
        - slug
    DocumentSearchHit:
      type: object
      description: 'One row of the search response: the document plus highlight snippets.'
      properties:
        document:
          allOf:
            - $ref: '#/components/schemas/Document'
          readOnly: true
        score:
          type: number
          format: double
          readOnly: true
        highlights:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
          readOnly: true
          description: >-
            Field → list of HTML snippets with ``<mark>`` tags around matched
            terms.  Currently populated for ``title`` and ``content``.
      required:
        - document
        - highlights
        - score
    DocumentSearchResponse:
      type: object
      properties:
        count:
          type: integer
        next:
          type: string
          nullable: true
        previous:
          type: string
          nullable: true
        results:
          type: array
          items:
            $ref: '#/components/schemas/DocumentSearchHit'
      required:
        - count
        - next
        - previous
        - results
    DocumentUpdate:
      type: object
      description: >-
        Write serializer for updating document metadata — allows re-linking
        patient.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        client:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        practice:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        patient:
          type: string
          format: uuid
          nullable: true
        category:
          type: string
          format: uuid
          nullable: true
        title:
          type: string
          maxLength: 255
        description:
          type: string
        original_filename:
          type: string
          readOnly: true
        mime_type:
          type: string
          readOnly: true
        file_size:
          type: integer
          readOnly: true
          description: File size in bytes
        page_count:
          type: integer
          readOnly: true
          nullable: true
          description: Page count for PDFs
        uploaded_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        source_application:
          type: integer
          readOnly: true
          nullable: true
          description: OAuth2 application that uploaded this document
        is_deleted:
          type: boolean
          readOnly: true
          description: Soft delete for HIPAA retention
        deleted_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        deleted_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        resolved_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: >-
            When this document was acknowledged at the practice level without
            being filed against a patient (api#511).  Used by the voicemail
            inbox triage UI: voicemails marked resolved drop out of the unlinked
            queue without being deleted.  Reversible — clearing the field
            returns the row to the queue.  Not a soft-delete: the underlying
            file is preserved and remains discoverable to the practice.
        resolved_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff member who marked the document resolved against the practice.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        download_url:
          type: string
          readOnly: true
        patient_name:
          type: string
          readOnly: true
        category_label:
          type: string
          readOnly: true
        uploaded_by_name:
          type: string
          readOnly: true
        caller_id:
          type: string
          description: >-
            E.164 caller-ID for voicemail recordings (e.g. +14155551234).  Used
            by the patient-link suggestion engine downstream of the
            voicemail-to-text pipeline.
          pattern: ^\+\d{1,15}$
          maxLength: 32
        processing_state:
          readOnly: true
          nullable: true
          description: >-
            Background-processing pipeline state for OCR / voicemail
            transcription.  Surfaces in the inbox UI via the existing
            documents_changed WebSocket event.  NULL on legacy rows that predate
            the state machine.


            * `pending` - Pending

            * `decoding` - Decoding

            * `loading_model` - Loading model

            * `processing` - Processing

            * `indexing` - Indexing

            * `complete` - Complete

            * `failed` - Failed
          oneOf:
            - $ref: '#/components/schemas/ProcessingStateEnum'
            - $ref: '#/components/schemas/NullEnum'
        processing_error:
          type: string
          readOnly: true
          description: >-
            Truncated str(exception) from the most recent failed pipeline run. 
            Plain text — never indexed.  Cleared on the next successful run.
        processing_updated_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When ``processing_state`` last changed.
        transcript:
          type: string
          readOnly: true
        transcript_confidence:
          type: string
          readOnly: true
        custom_data: {}
      required:
        - category_label
        - client
        - created
        - deleted_at
        - deleted_by
        - download_url
        - file_size
        - id
        - is_deleted
        - mime_type
        - modified
        - original_filename
        - page_count
        - patient_name
        - practice
        - processing_error
        - processing_state
        - processing_updated_at
        - resolved_at
        - resolved_by
        - source_application
        - title
        - transcript
        - transcript_confidence
        - uploaded_by
        - uploaded_by_name
    DocumentUpdateRequest:
      type: object
      description: >-
        Write serializer for updating document metadata — allows re-linking
        patient.
      properties:
        patient:
          type: string
          format: uuid
          nullable: true
        category:
          type: string
          format: uuid
          nullable: true
        title:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type: string
        caller_id:
          type: string
          description: >-
            E.164 caller-ID for voicemail recordings (e.g. +14155551234).  Used
            by the patient-link suggestion engine downstream of the
            voicemail-to-text pipeline.
          pattern: ^\+\d{1,15}$
          maxLength: 32
        custom_data: {}
      required:
        - title
    DocumentUploadSessionStatus:
      type: object
      description: Read-only session status for crash recovery.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        status:
          allOf:
            - $ref: '#/components/schemas/CaptureSessionStatusEnum'
          readOnly: true
        workstation_name:
          type: string
          readOnly: true
          description: Computer name from the capture workstation
        item_count:
          type: string
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - id
        - item_count
        - status
        - workstation_name
    EmailStatusEnum:
      enum:
        - NOT_SENT
        - QUEUED
        - SENT
        - DELIVERED
        - BOUNCED
        - COMPLAINED
        - FAILED
      type: string
      description: |-
        * `NOT_SENT` - Not Sent
        * `QUEUED` - Queued
        * `SENT` - Sent
        * `DELIVERED` - Delivered
        * `BOUNCED` - Bounced
        * `COMPLAINED` - Complained
        * `FAILED` - Failed
    EntityTypeEnum:
      enum:
        - definitions
        - providers
        - operatories
        - patients
        - recalls
        - appointments
        - balances
        - payments
        - insurance
        - commlogs
        - procedure_codes
        - schedules
        - planned_services
        - eod
        - procedures
        - adjustments
        - claims
        - treatment_plans
        - benefits
        - referrals
        - medical_history
        - payment_plans
        - patient_notes
        - carriers
        - appointment_procedures
        - treatment_plan_items
        - conditions
        - allergies
        - medications
        - vital_signs
        - tooth_conditions
        - perio_exams
        - perio_readings
        - clinical_notes
        - note_templates
        - patient_education
        - anesthesia_records
        - payment_records
        - payment_splits
        - payment_plan_charges
        - insurance_carriers
        - insurance_plans
        - fee_schedules
        - fee_schedule_entries
        - patient_insurance
        - insurance_claims
        - claim_procedures
        - insurance_benefits
        - comm_logs
        - schedule_templates
        - schedule_overrides
        - referral_sources
        - patient_referrals
        - portal_messages
        - imaging_sources
        - exam_templates
        - exam_template_slots
        - imaging_exams
        - imaging_exam_images
      type: string
      description: |-
        * `definitions` - definitions
        * `providers` - providers
        * `operatories` - operatories
        * `patients` - patients
        * `recalls` - recalls
        * `appointments` - appointments
        * `balances` - balances
        * `payments` - payments
        * `insurance` - insurance
        * `commlogs` - commlogs
        * `procedure_codes` - procedure_codes
        * `schedules` - schedules
        * `planned_services` - planned_services
        * `eod` - eod
        * `procedures` - procedures
        * `adjustments` - adjustments
        * `claims` - claims
        * `treatment_plans` - treatment_plans
        * `benefits` - benefits
        * `referrals` - referrals
        * `medical_history` - medical_history
        * `payment_plans` - payment_plans
        * `patient_notes` - patient_notes
        * `carriers` - carriers
        * `appointment_procedures` - appointment_procedures
        * `treatment_plan_items` - treatment_plan_items
        * `conditions` - conditions
        * `allergies` - allergies
        * `medications` - medications
        * `vital_signs` - vital_signs
        * `tooth_conditions` - tooth_conditions
        * `perio_exams` - perio_exams
        * `perio_readings` - perio_readings
        * `clinical_notes` - clinical_notes
        * `note_templates` - note_templates
        * `patient_education` - patient_education
        * `anesthesia_records` - anesthesia_records
        * `payment_records` - payment_records
        * `payment_splits` - payment_splits
        * `payment_plan_charges` - payment_plan_charges
        * `insurance_carriers` - insurance_carriers
        * `insurance_plans` - insurance_plans
        * `fee_schedules` - fee_schedules
        * `fee_schedule_entries` - fee_schedule_entries
        * `patient_insurance` - patient_insurance
        * `insurance_claims` - insurance_claims
        * `claim_procedures` - claim_procedures
        * `insurance_benefits` - insurance_benefits
        * `comm_logs` - comm_logs
        * `schedule_templates` - schedule_templates
        * `schedule_overrides` - schedule_overrides
        * `referral_sources` - referral_sources
        * `patient_referrals` - patient_referrals
        * `portal_messages` - portal_messages
        * `imaging_sources` - imaging_sources
        * `exam_templates` - exam_templates
        * `exam_template_slots` - exam_template_slots
        * `imaging_exams` - imaging_exams
        * `imaging_exam_images` - imaging_exam_images
    Exam:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        template:
          type: string
          format: uuid
          nullable: true
        template_name:
          type: string
          readOnly: true
        name:
          type: string
          description: Optional exam name/label
          maxLength: 255
        exam_date:
          type: string
          format: date
        provider:
          type: string
          format: uuid
          nullable: true
        provider_name:
          type: string
          readOnly: true
        notes:
          type: string
        is_complete:
          type: boolean
        image_count:
          type: integer
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - created
        - exam_date
        - id
        - image_count
        - modified
        - provider_name
        - template_name
    ExamDetail:
      type: object
      description: Detail view includes nested images.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        template:
          type: string
          format: uuid
          nullable: true
        template_name:
          type: string
          readOnly: true
        name:
          type: string
          description: Optional exam name/label
          maxLength: 255
        exam_date:
          type: string
          format: date
        provider:
          type: string
          format: uuid
          nullable: true
        provider_name:
          type: string
          readOnly: true
        notes:
          type: string
        is_complete:
          type: boolean
        image_count:
          type: integer
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
        images:
          type: array
          items:
            $ref: '#/components/schemas/ExamImage'
          readOnly: true
      required:
        - created
        - exam_date
        - id
        - image_count
        - images
        - modified
        - provider_name
        - template_name
    ExamImage:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        exam:
          type: string
          format: uuid
        source:
          type: string
          format: uuid
          nullable: true
        source_name:
          type: string
          readOnly: true
        template_slot:
          type: string
          format: uuid
          nullable: true
          description: Which slot in the exam template grid this image fills
        original_filename:
          type: string
          maxLength: 255
        mime_type:
          type: string
          maxLength: 50
        file_size:
          type: integer
          readOnly: true
        width:
          type: integer
          readOnly: true
        height:
          type: integer
          readOnly: true
        image_type:
          oneOf:
            - $ref: '#/components/schemas/ImageTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        capture_date:
          type: string
          format: date
        capture_device:
          type: string
          description: Device name from browser getUserMedia
          maxLength: 255
        tooth_numbers:
          description: 'Universal numbering: 1-32 permanent, A-T primary'
        description:
          type: string
          description: Short description/label for the image
          maxLength: 255
        degrees_rotated:
          type: integer
          maximum: 2147483647
          minimum: -2147483648
          description: 'Rotation state: 0, 90, 180, or 270 degrees'
        is_flipped:
          type: boolean
          description: Horizontal flip state
        display_settings:
          description: 'Viewer display settings: brightness, contrast, inverted, etc.'
        annotations:
          description: Array of annotation objects from the viewer
        notes:
          type: string
        is_deleted:
          type: boolean
          readOnly: true
        download_url:
          type: string
          readOnly: true
        thumbnail_url:
          type: string
          readOnly: true
        uploaded_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        deleted_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - capture_date
        - created
        - deleted_by
        - download_url
        - exam
        - file_size
        - height
        - id
        - is_deleted
        - modified
        - source_name
        - thumbnail_url
        - uploaded_by
        - width
    ExamImageRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        exam:
          type: string
          format: uuid
        source:
          type: string
          format: uuid
          nullable: true
        template_slot:
          type: string
          format: uuid
          nullable: true
          description: Which slot in the exam template grid this image fills
        original_filename:
          type: string
          maxLength: 255
        mime_type:
          type: string
          minLength: 1
          maxLength: 50
        image_type:
          oneOf:
            - $ref: '#/components/schemas/ImageTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        capture_date:
          type: string
          format: date
        capture_device:
          type: string
          description: Device name from browser getUserMedia
          maxLength: 255
        tooth_numbers:
          description: 'Universal numbering: 1-32 permanent, A-T primary'
        description:
          type: string
          description: Short description/label for the image
          maxLength: 255
        degrees_rotated:
          type: integer
          maximum: 2147483647
          minimum: -2147483648
          description: 'Rotation state: 0, 90, 180, or 270 degrees'
        is_flipped:
          type: boolean
          description: Horizontal flip state
        display_settings:
          description: 'Viewer display settings: brightness, contrast, inverted, etc.'
        annotations:
          description: Array of annotation objects from the viewer
        notes:
          type: string
        custom_data: {}
      required:
        - capture_date
        - exam
    ExamImageUpload:
      type: object
      description: Serializer for image upload -- accepts file via multipart.
      properties:
        exam:
          type: string
          format: uuid
        source:
          type: string
          format: uuid
          nullable: true
        template_slot:
          type: string
          format: uuid
          nullable: true
          description: Which slot in the exam template grid this image fills
        file:
          type: string
          format: uri
        image_type:
          oneOf:
            - $ref: '#/components/schemas/ImageTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        capture_date:
          type: string
          format: date
        capture_device:
          type: string
          description: Device name from browser getUserMedia
          maxLength: 255
        tooth_numbers:
          description: 'Universal numbering: 1-32 permanent, A-T primary'
        description:
          type: string
          description: Short description/label for the image
          maxLength: 255
        notes:
          type: string
      required:
        - capture_date
        - exam
        - file
    ExamImageUploadRequest:
      type: object
      description: Serializer for image upload -- accepts file via multipart.
      properties:
        exam:
          type: string
          format: uuid
        source:
          type: string
          format: uuid
          nullable: true
        template_slot:
          type: string
          format: uuid
          nullable: true
          description: Which slot in the exam template grid this image fills
        file:
          type: string
          format: binary
        image_type:
          oneOf:
            - $ref: '#/components/schemas/ImageTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        capture_date:
          type: string
          format: date
        capture_device:
          type: string
          description: Device name from browser getUserMedia
          maxLength: 255
        tooth_numbers:
          description: 'Universal numbering: 1-32 permanent, A-T primary'
        description:
          type: string
          description: Short description/label for the image
          maxLength: 255
        notes:
          type: string
      required:
        - capture_date
        - exam
        - file
    ExamRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        template:
          type: string
          format: uuid
          nullable: true
        name:
          type: string
          description: Optional exam name/label
          maxLength: 255
        exam_date:
          type: string
          format: date
        provider:
          type: string
          format: uuid
          nullable: true
        notes:
          type: string
        is_complete:
          type: boolean
        custom_data: {}
      required:
        - exam_date
    ExamTemplate:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          maxLength: 255
        description:
          type: string
        grid_rows:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Number of rows in the exam grid layout
        grid_cols:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Number of columns in the exam grid layout
        source:
          type: string
          format: uuid
          nullable: true
          description: Capture source for this template
        source_type:
          type: string
          readOnly: true
        is_active:
          type: boolean
        slots:
          type: array
          items:
            $ref: '#/components/schemas/ExamTemplateSlot'
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - created
        - id
        - modified
        - name
        - slots
        - source_type
    ExamTemplateRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type: string
        grid_rows:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Number of rows in the exam grid layout
        grid_cols:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Number of columns in the exam grid layout
        source:
          type: string
          format: uuid
          nullable: true
          description: Capture source for this template
        is_active:
          type: boolean
        custom_data: {}
      required:
        - name
    ExamTemplateSlot:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        row:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Row position in the grid (0-indexed)
        col:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Column position in the grid (0-indexed)
        label:
          type: string
          description: e.g., 'UR PA', 'BW Right', 'Photo 1'
          maxLength: 255
        custom_data: {}
      required:
        - id
    ExamTemplateSlotRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        row:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Row position in the grid (0-indexed)
        col:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Column position in the grid (0-indexed)
        label:
          type: string
          description: e.g., 'UR PA', 'BW Right', 'Photo 1'
          maxLength: 255
        custom_data: {}
    Fax:
      type: object
      description: |-
        Read serializer — list and detail.

        ``document_url`` is computed on every render so the URL never goes
        stale on a slow client; ``document`` (the raw key) is hidden so we
        don't leak the Spaces layout.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        direction:
          allOf:
            - $ref: '#/components/schemas/FaxDirectionEnum'
          readOnly: true
        from_number:
          type: string
          readOnly: true
          description: E.164 sender (may be empty for inbound spam).
        to_number:
          type: string
          readOnly: true
          description: E.164 destination.
        page_count:
          type: integer
          readOnly: true
          nullable: true
        status:
          allOf:
            - $ref: '#/components/schemas/FaxStatusEnum'
          readOnly: true
        sinch_fax_id:
          type: string
          readOnly: true
          description: >-
            Sinch's id for the fax — set once the carrier has accepted it
            (outbound) or notified us (inbound).
        document_url:
          type: string
          nullable: true
          description: >-
            Mint a signed URL for the PDF, or ``None`` if no document is
            attached.


            The Spaces backend signs with a 1-hour expiry by default; local

            FileSystemStorage returns an unsigned ``/media/...`` URL.  Either

            way, the caller treats the value as opaque and downloads from it

            directly — we never proxy bytes through Django.
          readOnly: true
        error_message:
          type: string
          readOnly: true
          description: Operator-facing failure reason — never includes upstream PHI.
        completed_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: Timestamp the fax reached a terminal state (DELIVERED or FAILED).
        sent_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff member who initiated an outbound fax.  NULL for inbound.
        patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: >-
            Patient this fax was filed against.  NULL for un-filed inbound faxes
            and for outbound faxes (which file themselves implicitly via the
            staff workflow that generated them).
        practice_link:
          type: boolean
          readOnly: true
          description: >-
            True when the fax was filed at the practice level rather than to a
            patient (api#508).  Mutually exclusive with ``patient`` — see the
            ``faxing_fax_filing_target`` check constraint.
        filed_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: >-
            When this fax was filed (against a patient or against the
            practice).  Populated by ``POST /faxes/<id>/file/``; used by the
            inbox ``?status=unfiled`` filter to find pending triage rows.
        filed_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff member who filed the fax (against patient or practice).
        discarded_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: >-
            When the fax was discarded (spam / wrong number).  Soft-flag — the
            underlying PDF is preserved for HIPAA retention and the row stays
            queryable for audit.
        discarded_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff member who discarded the fax.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - completed_at
        - created
        - direction
        - discarded_at
        - discarded_by
        - document_url
        - error_message
        - filed_at
        - filed_by
        - from_number
        - id
        - modified
        - page_count
        - patient
        - practice_link
        - sent_by
        - sinch_fax_id
        - status
        - to_number
    FaxCreate:
      type: object
      description: |-
        Write serializer for outbound fax submission.

        The view sets ``practice`` and ``sent_by`` from the URL + request
        user; the body only needs ``to_number`` and the PDF upload.  We
        accept ``from_number`` as an override (defaults to the practice's
        active fax number, resolved view-side) so a multi-location DSO can
        spoof a sub-practice's caller-id when its sub-account isn't routed
        yet — but not until the multi-number UX exists; today the field is
        ignored on read.
      properties:
        to_number:
          type: string
          description: E.164 destination.
          maxLength: 32
        document:
          type: string
          format: uri
          description: >-
            The PDF to fax.  Must be a single document; multi-doc batching is a
            phase-2 feature.
      required:
        - document
        - to_number
    FaxCreateRequest:
      type: object
      description: |-
        Write serializer for outbound fax submission.

        The view sets ``practice`` and ``sent_by`` from the URL + request
        user; the body only needs ``to_number`` and the PDF upload.  We
        accept ``from_number`` as an override (defaults to the practice's
        active fax number, resolved view-side) so a multi-location DSO can
        spoof a sub-practice's caller-id when its sub-account isn't routed
        yet — but not until the multi-number UX exists; today the field is
        ignored on read.
      properties:
        to_number:
          type: string
          minLength: 1
          description: E.164 destination.
          maxLength: 32
        document:
          type: string
          format: binary
          description: >-
            The PDF to fax.  Must be a single document; multi-doc batching is a
            phase-2 feature.
      required:
        - document
        - to_number
    FaxDirectionEnum:
      enum:
        - inbound
        - outbound
      type: string
      description: |-
        * `inbound` - Inbound
        * `outbound` - Outbound
    FaxFileRequest:
      type: object
      description: |-
        Body for ``POST /faxes/<id>/file/`` — file an inbound fax (api#508).

        The body shape follows the spec's principle of "one operation, one
        request": pass ``patient`` to file against a patient, omit it to file
        against the practice.  No ``practice: true`` boolean — its absence
        encodes the practice-link case so a malformed body can't accidentally
        file an inbound fax against the wrong target by misspelling a key.
      properties:
        patient:
          type: string
          format: uuid
          nullable: true
          description: >-
            UID of the patient to file the fax against.  Omit (or send ``null``)
            to file the fax against the practice.
    FaxProvisionRequestRequest:
      type: object
      description: |-
        Body for ``POST /fax/provision/`` (api#531).

        Both fields are optional: callers can omit the body entirely and let
        the view infer locality from the practice's address.  When either
        field is supplied it overrides the inferred value.
      properties:
        area_code:
          type: string
          description: >-
            US area code (3 digits, e.g. ``541``).  Sent to Sinch as the
            ``numberPattern`` prefix so the returned numbers all start with this
            code.  Omit to infer from the practice's phone / ZIP.
          maxLength: 3
        preferred_locality:
          type: string
          description: >-
            Free-form locality hint (e.g. ``"OR"`` or ``"Eugene, OR"``) recorded
            on the audit trail.  Sinch's Numbers API does not currently filter
            by city, so this is informational today.
          maxLength: 64
    FaxStatusEnum:
      enum:
        - queued
        - sending
        - sent
        - receiving
        - delivered
        - failed
      type: string
      description: |-
        * `queued` - Queued
        * `sending` - Sending
        * `sent` - Sent
        * `receiving` - Receiving
        * `delivered` - Delivered
        * `failed` - Failed
    FeeSchedule:
      type: object
      description: 'GH-711: ``carrier`` FK must belong to URL practice.'
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        carrier:
          type: string
          format: uuid
          nullable: true
        carrier_name:
          type: string
          readOnly: true
        name:
          type: string
          maxLength: 255
        effective_date:
          type: string
          format: date
          nullable: true
        expiration_date:
          type: string
          format: date
          nullable: true
        notes:
          type: string
        entry_count:
          type: integer
          readOnly: true
        entries:
          type: array
          items:
            $ref: '#/components/schemas/FeeScheduleEntry'
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - carrier_name
        - created
        - entries
        - entry_count
        - id
        - modified
        - name
    FeeScheduleEntry:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        proc_code:
          type: string
          maxLength: 20
        allowed_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        description:
          type: string
          description: Override description, falls back to ProcedureCode
          maxLength: 255
        custom_data: {}
      required:
        - allowed_amount
        - id
        - proc_code
    FeeScheduleEntryRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        proc_code:
          type: string
          minLength: 1
          maxLength: 20
        allowed_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        description:
          type: string
          description: Override description, falls back to ProcedureCode
          maxLength: 255
        custom_data: {}
      required:
        - allowed_amount
        - proc_code
    FeeScheduleList:
      type: object
      description: Lighter serializer for list views — no nested entries.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        carrier:
          type: string
          format: uuid
          nullable: true
        carrier_name:
          type: string
          readOnly: true
        name:
          type: string
          maxLength: 255
        effective_date:
          type: string
          format: date
          nullable: true
        expiration_date:
          type: string
          format: date
          nullable: true
        entry_count:
          type: integer
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - carrier_name
        - created
        - entry_count
        - id
        - modified
        - name
    FeeScheduleRequest:
      type: object
      description: 'GH-711: ``carrier`` FK must belong to URL practice.'
      properties:
        carrier:
          type: string
          format: uuid
          nullable: true
        name:
          type: string
          minLength: 1
          maxLength: 255
        effective_date:
          type: string
          format: date
          nullable: true
        expiration_date:
          type: string
          format: date
          nullable: true
        notes:
          type: string
        custom_data: {}
      required:
        - name
    FeedbackCreateRequest:
      type: object
      description: |-
        Validate and persist an inbound feedback submission.

        The widget posts a multipart body when a screenshot is attached
        and a JSON body otherwise; DRF's :class:`MultiPartParser` /
        :class:`JSONParser` handle the dispatch automatically.  The
        serializer accepts both shapes; the only field it explicitly
        refuses is ``user`` (set server-side from ``request.user`` so a
        client cannot misattribute the report).
      properties:
        practice:
          type: string
          format: uuid
          nullable: true
          description: Practice context at submission time, when known.
        title:
          type: string
          minLength: 1
          description: One-line summary the submitter typed into the feedback widget.
          maxLength: 80
        details:
          type: string
          minLength: 1
          description: Long-form description of the issue.
        screenshot:
          type: string
          format: binary
          description: >-
            Optional screenshot of the page the bug was observed on (private
            bucket).
        url:
          type: string
          description: Page URL the feedback was submitted from.
          maxLength: 2000
        viewport:
          description: Browser viewport metadata (width / height / devicePixelRatio).
        user_agent:
          type: string
          description: User-Agent string at submission time.
          maxLength: 1024
        js_errors:
          description: Recent JS console errors with timestamps (widget-owned shape).
      required:
        - details
        - title
    FeedbackDetail:
      type: object
      description: |-
        Detail (full) shape for a single feedback row.

        All fields are read-only.  Status changes go through
        :class:`apps.feedback.views.FeedbackTransitionView` so the
        triaged_by / triaged_at / resolved_by / resolved_at side effects
        fire consistently.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        user:
          type: string
          format: uuid
          readOnly: true
          description: >-
            Staff user who submitted the feedback (set server-side from
            request.user).
        submitter_email:
          type: string
          format: email
          readOnly: true
        practice:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Practice context at submission time, when known.
        client:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Client that owns the practice; auto-derived from practice on save.
        title:
          type: string
          readOnly: true
          description: One-line summary the submitter typed into the feedback widget.
        details:
          type: string
          readOnly: true
          description: Long-form description of the issue.
        screenshot_url:
          type: string
          nullable: true
          description: |-
            Return a short-lived signed URL for the screenshot, or None.

            ``get_presigned_url`` defaults to a 5-minute (300 s) TTL,
            which is short enough that a leaked URL has minimal residual
            risk and long enough that a slow image-rendering UI doesn't
            race the expiry.  Local-dev fallback returns the static media
            URL when DO Spaces is not configured (matches the doccenter /
            imaging behaviour).
          readOnly: true
        url:
          type: string
          readOnly: true
          description: Page URL the feedback was submitted from.
        viewport:
          readOnly: true
          description: Browser viewport metadata (width / height / devicePixelRatio).
        user_agent:
          type: string
          readOnly: true
          description: User-Agent string at submission time.
        js_errors:
          readOnly: true
          description: Recent JS console errors with timestamps (widget-owned shape).
        status:
          allOf:
            - $ref: '#/components/schemas/FeedbackStatusEnum'
          readOnly: true
          description: |-
            Triage state.

            * `new` - New
            * `triaged` - Triaged
            * `resolved` - Resolved
        triaged_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff user who first moved this row out of 'new'.
        triaged_by_email:
          type: string
          format: email
          readOnly: true
        triaged_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When the row was first moved out of 'new'.
        resolved_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff user who marked this row resolved.
        resolved_by_email:
          type: string
          format: email
          readOnly: true
        resolved_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When the row was marked resolved.
        resolution_note:
          type: string
          readOnly: true
          description: Free-form note attached on a status transition.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - client
        - created
        - details
        - id
        - js_errors
        - modified
        - practice
        - resolution_note
        - resolved_at
        - resolved_by
        - resolved_by_email
        - screenshot_url
        - status
        - submitter_email
        - title
        - triaged_at
        - triaged_by
        - triaged_by_email
        - url
        - user
        - user_agent
        - viewport
    FeedbackList:
      type: object
      description: |-
        Compact list shape for the support inbox.

        ``js_errors`` and ``viewport`` are omitted to keep list payloads
        small; open the detail view to see them.  ``submitter_email`` is
        denormalised so the inbox UI doesn't have to round-trip through
        ``/api/v1/admin/users/`` for every row.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        user:
          type: string
          format: uuid
          readOnly: true
          description: >-
            Staff user who submitted the feedback (set server-side from
            request.user).
        submitter_email:
          type: string
          format: email
          readOnly: true
        practice:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Practice context at submission time, when known.
        client:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Client that owns the practice; auto-derived from practice on save.
        title:
          type: string
          readOnly: true
          description: One-line summary the submitter typed into the feedback widget.
        url:
          type: string
          readOnly: true
          description: Page URL the feedback was submitted from.
        user_agent:
          type: string
          readOnly: true
          description: User-Agent string at submission time.
        status:
          allOf:
            - $ref: '#/components/schemas/FeedbackStatusEnum'
          readOnly: true
          description: |-
            Triage state.

            * `new` - New
            * `triaged` - Triaged
            * `resolved` - Resolved
        has_screenshot:
          type: boolean
          readOnly: true
        triaged_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff user who first moved this row out of 'new'.
        triaged_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When the row was first moved out of 'new'.
        resolved_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff user who marked this row resolved.
        resolved_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When the row was marked resolved.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - client
        - created
        - has_screenshot
        - id
        - modified
        - practice
        - resolved_at
        - resolved_by
        - status
        - submitter_email
        - title
        - triaged_at
        - triaged_by
        - url
        - user
        - user_agent
    FeedbackStatusEnum:
      enum:
        - new
        - triaged
        - resolved
      type: string
      description: |-
        * `new` - New
        * `triaged` - Triaged
        * `resolved` - Resolved
    FeedbackTransitionRequest:
      type: object
      description: |-
        Body shape for the status-transition endpoint.

        Two writable fields:

        * ``status`` -- target :class:`FeedbackStatus` value.  Required.
        * ``resolution_note`` -- optional free-form text attached to the
          transition; surfaces in the detail view.  Defaults to empty
          string so existing notes are preserved if the caller omits the
          key (DRF would otherwise treat ``None`` as a clear).
      properties:
        status:
          $ref: '#/components/schemas/FeedbackStatusEnum'
        resolution_note:
          type: string
          default: ''
      required:
        - status
    FieldTypeEnum:
      enum:
        - text
        - textarea
        - yes_no
        - date
        - signature
        - allergies
        - medications
        - dob
        - email
        - phone
        - address
      type: string
      description: |-
        * `text` - Single-line text
        * `textarea` - Multi-line text
        * `yes_no` - Yes / No
        * `date` - Date
        * `signature` - Signature
        * `allergies` - Allergies (structured list)
        * `medications` - Medications (structured list)
        * `dob` - Date of birth
        * `email` - Email address
        * `phone` - Phone number
        * `address` - Street address (structured)
    FirstRunTourCompleteResponse:
      type: object
      description: |-
        Tiny response envelope for :class:`FirstRunTourCompleteView`.

        Used purely so drf-spectacular emits a typed schema component for
        the response body; the field shape mirrors what callers care about
        (the timestamp, plus an explicit ``already_completed`` flag so the
        client can distinguish "I just flipped the latch" from "I retried
        a confirmed write").
      properties:
        first_run_tour_completed_at:
          type: string
          format: date-time
          description: UTC timestamp the latch was flipped (now() on first call).
        already_completed:
          type: boolean
          description: >-
            False on the call that actually stamped the timestamp; True on every
            idempotent re-call thereafter.  Lets the client log or short-circuit
            retries without a separate read.
      required:
        - already_completed
        - first_run_tour_completed_at
    FormBundle:
      type: object
      description: |-
        Practice-scoped bundle with ordered template membership.

        Write payload accepts ``template_ids`` as an ordered list; reads expose
        ``items`` with the joined template name for display.

        ``is_default_new_patient`` is writable (api#769) — setting it true
        clears the flag on any sibling bundle in the same practice as a
        side-effect of :meth:`FormBundle.save`.  ``template_ids`` is required
        on create but optional on PATCH so a flag-only edit doesn't have to
        re-submit the entire ordered membership.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          maxLength: 200
        description:
          type: string
        is_default_new_patient:
          type: boolean
          description: >-
            When true, this bundle is the practice's default new-patient intake
            packet (api#769).  Setting this true on a row clears the flag on any
            sibling bundle in the same practice — at most one default per
            practice.  False on any number of rows is fine.
        items:
          type: array
          items:
            $ref: '#/components/schemas/FormBundleItemRead'
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - id
        - items
        - modified
        - name
    FormBundleItemRead:
      type: object
      description: Bundle membership as returned to the client.
      properties:
        template:
          type: string
          format: uuid
        template_name:
          type: string
          readOnly: true
        order:
          type: integer
          maximum: 2147483647
          minimum: 0
      required:
        - template
        - template_name
    FormBundleItemReadRequest:
      type: object
      description: Bundle membership as returned to the client.
      properties:
        template:
          type: string
          format: uuid
        order:
          type: integer
          maximum: 2147483647
          minimum: 0
      required:
        - template
    FormBundleRequest:
      type: object
      description: |-
        Practice-scoped bundle with ordered template membership.

        Write payload accepts ``template_ids`` as an ordered list; reads expose
        ``items`` with the joined template name for display.

        ``is_default_new_patient`` is writable (api#769) — setting it true
        clears the flag on any sibling bundle in the same practice as a
        side-effect of :meth:`FormBundle.save`.  ``template_ids`` is required
        on create but optional on PATCH so a flag-only edit doesn't have to
        re-submit the entire ordered membership.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
        is_default_new_patient:
          type: boolean
          description: >-
            When true, this bundle is the practice's default new-patient intake
            packet (api#769).  Setting this true on a row clears the flag on any
            sibling bundle in the same practice — at most one default per
            practice.  False on any number of rows is fine.
        template_ids:
          type: array
          items:
            type: string
            format: uuid
          writeOnly: true
          description: Ordered list of FormTemplate UUIDs to include.
      required:
        - name
    FormField:
      type: object
      description: Read/write serializer for individual fields on a template.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        order:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Display order within the template.
        field_type:
          $ref: '#/components/schemas/FieldTypeEnum'
        label:
          type: string
          maxLength: 255
        help_text:
          type: string
        required:
          type: boolean
        options:
          description: Choice options, validation rules, or widget config (per field_type).
        maps_to_patient_field:
          type: string
          description: >-
            Optional name of a Patient model field to receive this value (e.g.
            'date_of_birth', 'email'). Phase 1 stores the mapping but does not
            auto-apply — staff must approve. See escape#156.
          maxLength: 64
        consent_text:
          type: string
          description: >-
            Legal text the patient sees above the signature pad. Used verbatim
            as the Signature.consent_text_snapshot at submit time (api#621) so a
            later edit of the template cannot retroactively alter what the
            signer agreed to. Only meaningful for field_type='signature'.
      required:
        - field_type
        - id
        - label
    FormFieldMappingProposal:
      type: object
      description: |-
        Read-only proposal payload for staff review.

        The denormalised ``patient``, ``submission_template`` and
        ``field_label`` fields exist so the approval UI can render a useful
        list without N+1 queries against the form / submission graph.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        submission:
          type: string
          format: uuid
          readOnly: true
        submission_template:
          type: string
          format: uuid
          readOnly: true
        field_response:
          type: string
          format: uuid
          readOnly: true
          description: The single response this proposal stages a write for.
        field_label:
          type: string
          readOnly: true
        field_type:
          type: string
          readOnly: true
        target_patient_field:
          type: string
          readOnly: true
          description: >-
            Name of the Patient field (or structured stream key) to receive the
            value.
        proposed_value:
          type: string
          readOnly: true
          description: >-
            Raw response value; JSON-encoded text for structured types
            (allergies/medications/address).
        current_patient_value_snapshot:
          type: string
          readOnly: true
          description: >-
            What the patient row held at submission time. Empty string when the
            current value was null/blank or the structured snapshot serialised
            to '[]'.
        status:
          allOf:
            - $ref: '#/components/schemas/FormFieldMappingProposalStatusEnum'
          readOnly: true
        reviewed_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        reviewed_by_email:
          type: string
          format: email
          readOnly: true
        reviewed_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        review_notes:
          type: string
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        practice:
          type: string
          format: uuid
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - current_patient_value_snapshot
        - field_label
        - field_response
        - field_type
        - id
        - modified
        - patient
        - practice
        - proposed_value
        - review_notes
        - reviewed_at
        - reviewed_by
        - reviewed_by_email
        - status
        - submission
        - submission_template
        - target_patient_field
    FormFieldMappingProposalStatusEnum:
      enum:
        - pending
        - approved
        - rejected
      type: string
      description: |-
        * `pending` - Pending review
        * `approved` - Approved — written to patient record
        * `rejected` - Rejected — patient record unchanged
    FormFieldRequest:
      type: object
      description: Read/write serializer for individual fields on a template.
      properties:
        order:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Display order within the template.
        field_type:
          $ref: '#/components/schemas/FieldTypeEnum'
        label:
          type: string
          minLength: 1
          maxLength: 255
        help_text:
          type: string
        required:
          type: boolean
        options:
          description: Choice options, validation rules, or widget config (per field_type).
        maps_to_patient_field:
          type: string
          description: >-
            Optional name of a Patient model field to receive this value (e.g.
            'date_of_birth', 'email'). Phase 1 stores the mapping but does not
            auto-apply — staff must approve. See escape#156.
          maxLength: 64
        consent_text:
          type: string
          description: >-
            Legal text the patient sees above the signature pad. Used verbatim
            as the Signature.consent_text_snapshot at submit time (api#621) so a
            later edit of the template cannot retroactively alter what the
            signer agreed to. Only meaningful for field_type='signature'.
      required:
        - field_type
        - label
    FormSendOutCreate:
      type: object
      description: |-
        Staff-side creation of a sendout for a patient.

        Exactly one of ``bundle`` / ``template`` must be set — enforced both at
        the serializer level (clear validation errors) and via the model's
        CheckConstraint (defence in depth).

        ``channel`` (api#748) defaults to ``email`` to keep callers that
        haven't updated yet on the historical behaviour.  Set to ``sms``
        to dispatch via Twilio or ``kiosk`` for in-office tablet handoff.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
        bundle:
          type: string
          format: uuid
          nullable: true
        template:
          type: string
          format: uuid
          nullable: true
          description: Set when sending exactly one template without a bundle.
        channel:
          allOf:
            - $ref: '#/components/schemas/ChannelEnum'
          description: >-
            Delivery channel for the patient-facing link (api#748). Email and
            SMS dispatch a Celery notification at create + resend; kiosk is a
            no-op (staff loads the sendout from the kiosk control panel).


            * `email` - Email

            * `sms` - SMS

            * `kiosk` - In-office kiosk
        token:
          type: string
          readOnly: true
          description: Opaque UUID-hex token threaded into the patient's signed URL.
        status:
          allOf:
            - $ref: '#/components/schemas/Status4d1Enum'
          readOnly: true
        expires_at:
          type: string
          format: date-time
        public_url:
          type: string
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - id
        - patient
        - public_url
        - status
        - token
    FormSendOutCreateRequest:
      type: object
      description: |-
        Staff-side creation of a sendout for a patient.

        Exactly one of ``bundle`` / ``template`` must be set — enforced both at
        the serializer level (clear validation errors) and via the model's
        CheckConstraint (defence in depth).

        ``channel`` (api#748) defaults to ``email`` to keep callers that
        haven't updated yet on the historical behaviour.  Set to ``sms``
        to dispatch via Twilio or ``kiosk`` for in-office tablet handoff.
      properties:
        patient:
          type: string
          format: uuid
        bundle:
          type: string
          format: uuid
          nullable: true
        template:
          type: string
          format: uuid
          nullable: true
          description: Set when sending exactly one template without a bundle.
        channel:
          allOf:
            - $ref: '#/components/schemas/ChannelEnum'
          description: >-
            Delivery channel for the patient-facing link (api#748). Email and
            SMS dispatch a Celery notification at create + resend; kiosk is a
            no-op (staff loads the sendout from the kiosk control panel).


            * `email` - Email

            * `sms` - SMS

            * `kiosk` - In-office kiosk
        expires_at:
          type: string
          format: date-time
      required:
        - patient
    FormSendOutList:
      type: object
      description: |-
        Read-only sendout view for staff list / retrieve.

        Surfaced on both the list endpoint (paginated) and the per-row
        detail endpoint (api#654).  The shape is intentionally identical
        so the web client can hand a list-row to the detail page and only
        re-fetch on demand.  ``last_resent_at`` (api#655) is included so
        the detail page can render "last reminded N minutes ago" and the
        web client can gray out the resend button until the throttle
        window has elapsed without a second round-trip.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        patient_name:
          type: string
          readOnly: true
        bundle:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        bundle_name:
          type: string
          readOnly: true
        template:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Set when sending exactly one template without a bundle.
        template_name:
          type: string
          readOnly: true
        channel:
          allOf:
            - $ref: '#/components/schemas/ChannelEnum'
          readOnly: true
          description: >-
            Delivery channel for the patient-facing link (api#748). Email and
            SMS dispatch a Celery notification at create + resend; kiosk is a
            no-op (staff loads the sendout from the kiosk control panel).


            * `email` - Email

            * `sms` - SMS

            * `kiosk` - In-office kiosk
        token:
          type: string
          readOnly: true
          description: Opaque UUID-hex token threaded into the patient's signed URL.
        status:
          allOf:
            - $ref: '#/components/schemas/Status4d1Enum'
          readOnly: true
        expires_at:
          type: string
          format: date-time
          readOnly: true
        opened_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        submitted_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        last_resent_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: >-
            Most recent successful staff-triggered resend of the patient
            notification (api#655). Used by the resend endpoint to throttle
            button-mashes to at most one delivery per hour per sendout. NULL on
            rows that have never been resent (the initial delivery does NOT
            populate this field — it tracks RE-sends only).
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - bundle
        - bundle_name
        - channel
        - created
        - expires_at
        - id
        - last_resent_at
        - opened_at
        - patient
        - patient_name
        - status
        - submitted_at
        - template
        - template_name
        - token
    FormSessionSpecRequest:
      type: object
      description: >-
        Validate the ``form_session`` block on a send-to-tablet POST (api#771 /
        api#787).


        Shape-only -- the cross-practice FK validation (bundle / templates /

        patient / appointment must live at the calling practice) happens in

        the view where the practice is bound from the URL.  The

        serializer's job is to make sure the field set is the right shape

        and the types parse; semantic refusals belong with the practice

        scope.


        Either ``bundle_id`` OR ``template_ids`` is required -- a caller

        that supplies both, or neither, gets a 400 keyed on the offending

        block.  ``bundle_id`` resolves a stored :class:`FormBundle`'s

        ordered templates at send time; ``template_ids`` is the

        "ad-hoc bundle" path added in api#787 for one-off single- or

        multi-template sends where staff doesn't want to mint a FormBundle

        row just to ship a single form to the tablet.


        ``bundle_id`` is the :class:`apps.forms.models.FormBundle` PK -- a

        UUID via the project-wide BaseModel.  ``template_ids`` is a list of

        :class:`apps.forms.models.FormTemplate` PKs (also UUIDs).

        ``patient_id`` and ``appointment_id`` are also UUIDs (BaseModel's

        UUID PK on those models).  Both patient and appointment are

        optional -- a new-patient walk-up flow has no patient FK yet, and

        not every session is appointment-scoped.
      properties:
        bundle_id:
          type: string
          format: uuid
          nullable: true
          description: >-
            FormBundle.id to resolve at send time.  Mutually exclusive with
            template_ids.
        template_ids:
          type: array
          items:
            type: string
            format: uuid
          description: >-
            Ordered list of FormTemplate.id values for an ad-hoc bundle
            (api#787).  Mutually exclusive with bundle_id.  Order is preserved
            end-to-end -- the tablet renders templates in the sequence supplied.
          maxItems: 20
        patient_id:
          type: string
          format: uuid
          nullable: true
          description: Patient.id; null/omitted for new-patient walk-up flows.
        appointment_id:
          type: string
          format: uuid
          nullable: true
          description: Appointment.id; null/omitted for non-visit-scoped sessions.
    FormSessionSubmitRequestRequest:
      type: object
      description: |-
        Request body for the form-session submit endpoint.

        The renderer keys ``values`` by field label (the tablet runs the
        template render before the patient even touches the screen, so a
        field-uuid-keyed payload would force the tablet to round-trip the
        template definition on every save).  We accept the dict as-is and
        persist it verbatim onto :attr:`FormSubmission.values` -- the
        serializer guard only enforces top-level shape.
      properties:
        session_id:
          type: string
          format: uuid
          description: >-
            form_session correlation UUID returned by the staff-side send/
            endpoint at session creation (api#771).  Must match the action the
            tablet is currently riding.
        template_id:
          type: string
          format: uuid
          description: The :class:`FormTemplate` this submission answers.
        values:
          description: >-
            Field-label -> value map the tablet captured.  Persisted verbatim
            onto FormSubmission.values; tablet writebacks do NOT spawn per-field
            FormFieldResponse rows.
        signature_b64:
          type: string
          default: ''
          description: >-
            Optional base64-encoded signature image captured at submit time.
            Decoded server-side and persisted as a Signature row attached to the
            submission's GFK so the legal evidence row survives the submission's
            lifecycle.  Accepts a bare base64 payload OR a full
            ``data:image/png;base64,...`` URL.
      required:
        - session_id
        - template_id
    FormSessionSubmitResponse:
      type: object
      description: |-
        Response body for the form-session submit endpoint.

        The shape stays the same whether a row was created (201) or replayed
        (200), so the tablet's client code can ignore the status difference
        if it wants to.  ``draft_status`` is populated only for new-patient
        bundles; known-patient bundles return ``patient_id`` instead.
      properties:
        submission_id:
          type: string
          format: uuid
        session_id:
          type: string
          format: uuid
        template_id:
          type: string
          format: uuid
        patient_id:
          type: string
          format: uuid
          nullable: true
        new_patient_draft_id:
          type: string
          format: uuid
          nullable: true
        draft_status:
          type: string
          nullable: true
        bundle_completed:
          type: boolean
      required:
        - bundle_completed
        - new_patient_draft_id
        - patient_id
        - session_id
        - submission_id
        - template_id
    FormSubmission:
      type: object
      description: |-
        Read-only submission view for the per-sendout submission list (api#656).

        The list endpoint at ``/forms/sendouts/<id>/submissions/`` and the
        convenience ``/.../latest/`` endpoint both return rows shaped by
        this serializer.  ``attached_document`` is surfaced as the raw FK
        UUID (with a sibling ``attached_document_url`` for clients that
        want to render a "Download submitted PDF" button without an extra
        round-trip).  We do NOT inline the FormFieldResponse rows here —
        the response collection is large enough that putting it inline
        would balloon list payloads; a dedicated ``submissions/<id>/``
        endpoint (or the existing PDF route) is the right surface for the
        full response set.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        sendout:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: >-
            Owning sendout for sendout-driven submissions (api#613). NULL on
            tablet form-session submissions (api#773) — those carry session_id +
            patient_or_draft instead.
        template:
          type: string
          format: uuid
          readOnly: true
          description: The exact (versioned) template the patient submitted against.
        template_name:
          type: string
          readOnly: true
        template_version:
          type: integer
          readOnly: true
        submitted_at:
          type: string
          format: date-time
          readOnly: true
        submitted_via:
          allOf:
            - $ref: '#/components/schemas/FormSubmissionSubmittedViaEnum'
          readOnly: true
        applied_to_patient:
          type: boolean
          readOnly: true
          description: >-
            True after a staff member approved field-to-patient writes (Phase
            2).
        attached_document:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: >-
            Rendered-PDF snapshot of this submission, filed against the
            patient's chart via the doccenter pipeline (api#620).  Populated
            asynchronously by ``forms.tasks.attach_submission_pdf_to_chart``
            after the patient submits.  NULL on rows whose attach task has not
            yet completed or whose Document was hard-deleted (SET_NULL preserves
            the submission).
        attached_document_url:
          type: string
          nullable: true
          description: |-
            Return the PDF download URL for the auto-attached chart document.

            Routes through the existing ``submissions/<id>/pdf/`` endpoint
            rather than a doccenter-direct URL so the audit-log emission
            on the PDF view stays the single source of truth for "staff
            downloaded this submission".  Returns ``None`` while the
            Celery attach task has not yet completed (or the underlying
            Document was hard-deleted).
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - applied_to_patient
        - attached_document
        - attached_document_url
        - created
        - id
        - sendout
        - submitted_at
        - submitted_via
        - template
        - template_name
        - template_version
    FormSubmissionSubmittedViaEnum:
      enum:
        - portal
        - kiosk
        - link
      type: string
      description: |-
        * `portal` - Patient portal
        * `kiosk` - In-office kiosk
        * `link` - Public signed link
    FormTemplate:
      type: object
      description: |-
        Practice-scoped form template with nested fields.

        A ``PATCH`` against a template that has any submissions tied to it bumps
        the version (archives the current row, returns the new one).  Edits on
        a template with no submissions update in place — there's no audit value
        in versioning a draft that no patient has touched.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        version_group:
          type: string
          format: uuid
          readOnly: true
          description: Shared across every version of a logically-identical template.
        version:
          type: integer
          readOnly: true
          default: 1
          description: >-
            Monotonically increasing per version_group. Edits archive the
            previous row.
        name:
          type: string
          maxLength: 200
        description:
          type: string
        is_active:
          type: boolean
          readOnly: true
          description: >-
            False for archived prior versions. Only one row per version_group is
            active at a time.
        fields:
          type: array
          items:
            $ref: '#/components/schemas/FormField'
        prefill_map:
          description: >-
            Map of FormField.label (or template field name) -> dotted attribute
            path on the Patient row that should pre-populate that field on the
            kiosk / portal renderer (api#772).  Shape: ``{field_name:
            'patient.attribute_dotted_path'}``. Example: ``{'First name':
            'first_name', 'Insurance member ID':
            'insurance_enrollments.first.subscriber_id'}``.  Resolution is
            best-effort and read-only -- absent attributes / null values are
            omitted from the response so the renderer treats them as 'user must
            fill in'.  See :func:`apps.forms.prefill.resolve_prefill` for
            traversal rules.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - id
        - is_active
        - modified
        - name
        - version
        - version_group
    FormTemplateRequest:
      type: object
      description: |-
        Practice-scoped form template with nested fields.

        A ``PATCH`` against a template that has any submissions tied to it bumps
        the version (archives the current row, returns the new one).  Edits on
        a template with no submissions update in place — there's no audit value
        in versioning a draft that no patient has touched.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
        fields:
          type: array
          items:
            $ref: '#/components/schemas/FormFieldRequest'
        prefill_map:
          description: >-
            Map of FormField.label (or template field name) -> dotted attribute
            path on the Patient row that should pre-populate that field on the
            kiosk / portal renderer (api#772).  Shape: ``{field_name:
            'patient.attribute_dotted_path'}``. Example: ``{'First name':
            'first_name', 'Insurance member ID':
            'insurance_enrollments.first.subscriber_id'}``.  Resolution is
            best-effort and read-only -- absent attributes / null values are
            omitted from the response so the renderer treats them as 'user must
            fill in'.  See :func:`apps.forms.prefill.resolve_prefill` for
            traversal rules.
      required:
        - name
    FormatEnum:
      enum:
        - pdf
        - json
        - both
      type: string
      description: |-
        * `pdf` - pdf
        * `json` - json
        * `both` - both
    GenderEnum:
      enum:
        - male
        - female
        - other
        - unknown
      type: string
      description: |-
        * `male` - Male
        * `female` - Female
        * `other` - Other
        * `unknown` - Unknown
    GradeEnum:
      enum:
        - grade_a
        - grade_b
        - grade_c
      type: string
      description: |-
        * `grade_a` - Grade A - Slow
        * `grade_b` - Grade B - Moderate
        * `grade_c` - Grade C - Rapid
    HygieneRecarePracticeRow:
      type: object
      description: Per-practice hygiene-recare row.
      properties:
        practice_id:
          type: string
        practice_name:
          type: string
        compliant_count:
          type: integer
        upcoming_count:
          type: integer
        overdue_count:
          type: integer
        without_active_recare_count:
          type: integer
        recall_total:
          type: integer
        compliance_rate_pct:
          type: string
        avg_days_late_among_non_compliant:
          type: string
      required:
        - avg_days_late_among_non_compliant
        - compliance_rate_pct
        - compliant_count
        - overdue_count
        - practice_id
        - practice_name
        - recall_total
        - upcoming_count
        - without_active_recare_count
    HygieneRecareResponse:
      type: object
      description: Response for ``/reports/hygiene-recare/``.
      properties:
        as_of:
          type: string
        totals:
          $ref: '#/components/schemas/_RecareTotals'
        practices:
          type: array
          items:
            $ref: '#/components/schemas/HygieneRecarePracticeRow'
      required:
        - as_of
        - practices
        - totals
    ImageMount:
      type: object
      description: |-
        List/create serializer for mounts — slots are read-only here.

        Creating a mount with ``template`` will eagerly create one empty
        ``ImageMountSlot`` per tile in the template layout; this happens in
        the view's ``perform_create`` so the serializer stays deterministic.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
        exam:
          type: string
          format: uuid
          nullable: true
          description: Optional exam the mount was initialised from.
        template:
          type: string
          format: uuid
        template_code:
          type: string
          readOnly: true
        template_name:
          type: string
          readOnly: true
        name:
          type: string
          description: Optional user-supplied label.
          maxLength: 255
        notes:
          type: string
        slots:
          type: array
          items:
            $ref: '#/components/schemas/ImageMountSlot'
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - created
        - id
        - modified
        - patient
        - slots
        - template
        - template_code
        - template_name
    ImageMountRequest:
      type: object
      description: |-
        List/create serializer for mounts — slots are read-only here.

        Creating a mount with ``template`` will eagerly create one empty
        ``ImageMountSlot`` per tile in the template layout; this happens in
        the view's ``perform_create`` so the serializer stays deterministic.
      properties:
        patient:
          type: string
          format: uuid
        exam:
          type: string
          format: uuid
          nullable: true
          description: Optional exam the mount was initialised from.
        template:
          type: string
          format: uuid
        name:
          type: string
          description: Optional user-supplied label.
          maxLength: 255
        notes:
          type: string
        custom_data: {}
      required:
        - patient
        - template
    ImageMountSlot:
      type: object
      description: |-
        Serializer for a single slot on a mount.

        Used both when the slot is read (nested on the mount detail response)
        and when a slot is updated via PATCH. Image assignment validates the
        image belongs to the same patient as the mount.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        slot_index:
          type: integer
          readOnly: true
          description: 1-based tile index within the mount's template layout.
        image:
          type: string
          format: uuid
          nullable: true
        rotation:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: 'Display rotation in degrees: 0, 90, 180 or 270.'
        flipped:
          type: boolean
          description: Horizontal flip.
        label:
          type: string
          maxLength: 255
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - created
        - id
        - modified
        - slot_index
    ImageMountSlotRequest:
      type: object
      description: |-
        Serializer for a single slot on a mount.

        Used both when the slot is read (nested on the mount detail response)
        and when a slot is updated via PATCH. Image assignment validates the
        image belongs to the same patient as the mount.
      properties:
        image:
          type: string
          format: uuid
          nullable: true
        rotation:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: 'Display rotation in degrees: 0, 90, 180 or 270.'
        flipped:
          type: boolean
          description: Horizontal flip.
        label:
          type: string
          maxLength: 255
        custom_data: {}
    ImageTypeEnum:
      enum:
        - pa
        - bw
        - pano
        - ceph
        - cbct
        - intraoral_photo
        - extraoral_photo
        - other
      type: string
      description: |-
        * `pa` - Periapical
        * `bw` - Bitewing
        * `pano` - Panoramic
        * `ceph` - Cephalometric
        * `cbct` - CBCT
        * `intraoral_photo` - Intraoral Photo
        * `extraoral_photo` - Extraoral Photo
        * `other` - Other
    ImportBatchRequest:
      type: object
      description: |-
        Payload for a batch import request.

        The ``records`` list contains raw dicts whose shape depends on the
        entity type.  Per-field validation is done in the view layer when
        mapping records onto model instances.
      properties:
        session_id:
          type: string
          format: uuid
        entity_type:
          $ref: '#/components/schemas/EntityTypeEnum'
        records:
          type: array
          items:
            type: object
            additionalProperties: {}
      required:
        - entity_type
        - records
        - session_id
    ImportBatchResponse:
      type: object
      description: Response from ``POST /sync/sessions/<id>/batch/<entity_type>/``.
      properties:
        created:
          type: integer
        skipped:
          type: integer
        errors:
          type: integer
        error_messages:
          type: array
          items:
            type: string
      required:
        - created
        - error_messages
        - errors
        - skipped
    ImportIssueCreate:
      type: object
      description: |-
        Validate and persist an inbound importer error report.

        The importer-shaped payload mirrors the field layout 1:1 with the
        model so a ``ModelSerializer`` is the right tool here -- there is
        no semantic translation between request shape and storage shape.
        Status / assigned_to / resolution_note are deliberately excluded:
        new reports always land in :attr:`ImportIssueStatus.NEW` and the
        inbox UI assigns + resolves them later.
      properties:
        importer:
          type: string
          description: Slug of the reporting importer, e.g. 'escape-eaglesoft'.
          maxLength: 64
        importer_version:
          type: string
          description: Self-reported version of the importer that raised the issue.
          maxLength: 64
        pms:
          type: string
          description: Slug of the upstream practice management system.
          maxLength: 32
        pms_version:
          type: string
          description: Self-reported PMS version, when the importer can read it.
          maxLength: 64
        practice_uid:
          type: string
          format: uuid
          nullable: true
          description: Practice UUID the importer was syncing, when established.
        client_uid:
          type: string
          format: uuid
          nullable: true
          description: Client (parent of practice) UUID, when established.
        kind:
          allOf:
            - $ref: '#/components/schemas/KindEnum'
          description: |-
            Failure class (schema change / data shape / runtime).

            * `schema_change` - Schema change
            * `data_shape` - Data shape surprise
            * `runtime` - Runtime error
        occurred_at:
          type: string
          format: date-time
          description: Timestamp the importer recorded the error (importer's clock).
        summary:
          type: string
          description: One-line description for the inbox list view (PHI-free).
          maxLength: 500
        context:
          description: Structured failure payload (PHI permitted; staff-only on read).
      required:
        - importer
        - kind
        - occurred_at
        - pms
        - summary
    ImportIssueCreateRequest:
      type: object
      description: |-
        Validate and persist an inbound importer error report.

        The importer-shaped payload mirrors the field layout 1:1 with the
        model so a ``ModelSerializer`` is the right tool here -- there is
        no semantic translation between request shape and storage shape.
        Status / assigned_to / resolution_note are deliberately excluded:
        new reports always land in :attr:`ImportIssueStatus.NEW` and the
        inbox UI assigns + resolves them later.
      properties:
        importer:
          type: string
          minLength: 1
          description: Slug of the reporting importer, e.g. 'escape-eaglesoft'.
          maxLength: 64
        importer_version:
          type: string
          description: Self-reported version of the importer that raised the issue.
          maxLength: 64
        pms:
          type: string
          minLength: 1
          description: Slug of the upstream practice management system.
          maxLength: 32
        pms_version:
          type: string
          description: Self-reported PMS version, when the importer can read it.
          maxLength: 64
        practice_uid:
          type: string
          format: uuid
          nullable: true
          description: Practice UUID the importer was syncing, when established.
        client_uid:
          type: string
          format: uuid
          nullable: true
          description: Client (parent of practice) UUID, when established.
        kind:
          allOf:
            - $ref: '#/components/schemas/KindEnum'
          description: |-
            Failure class (schema change / data shape / runtime).

            * `schema_change` - Schema change
            * `data_shape` - Data shape surprise
            * `runtime` - Runtime error
        occurred_at:
          type: string
          format: date-time
          description: Timestamp the importer recorded the error (importer's clock).
        summary:
          type: string
          minLength: 1
          description: One-line description for the inbox list view (PHI-free).
          maxLength: 500
        context:
          description: Structured failure payload (PHI permitted; staff-only on read).
      required:
        - importer
        - kind
        - occurred_at
        - pms
        - summary
    ImportIssueDetail:
      type: object
      description: |-
        Detail / PATCH shape for the inbox.

        Writable fields on PATCH: ``status``, ``assigned_to``,
        ``resolution_note``.  Everything else is read-only -- the report
        body is immutable after intake.  Read-only ``assigned_to_email``
        is denormalised so the inbox UI doesn't have to round-trip through
        ``/api/v1/admin/users/`` for every row it renders.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        importer:
          type: string
          readOnly: true
          description: Slug of the reporting importer, e.g. 'escape-eaglesoft'.
        importer_version:
          type: string
          readOnly: true
          description: Self-reported version of the importer that raised the issue.
        pms:
          type: string
          readOnly: true
          description: Slug of the upstream practice management system.
        pms_version:
          type: string
          readOnly: true
          description: Self-reported PMS version, when the importer can read it.
        practice_uid:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Practice UUID the importer was syncing, when established.
        client_uid:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Client (parent of practice) UUID, when established.
        kind:
          allOf:
            - $ref: '#/components/schemas/KindEnum'
          readOnly: true
          description: |-
            Failure class (schema change / data shape / runtime).

            * `schema_change` - Schema change
            * `data_shape` - Data shape surprise
            * `runtime` - Runtime error
        occurred_at:
          type: string
          format: date-time
          readOnly: true
          description: Timestamp the importer recorded the error (importer's clock).
        summary:
          type: string
          readOnly: true
          description: One-line description for the inbox list view (PHI-free).
        context:
          readOnly: true
          description: Structured failure payload (PHI permitted; staff-only on read).
        status:
          allOf:
            - $ref: '#/components/schemas/ImportIssueStatusEnum'
          description: |-
            Triage state.

            * `new` - New
            * `triaged` - Triaged
            * `resolved` - Resolved
            * `wontfix` - Won't fix
        assigned_to:
          type: string
          format: uuid
          nullable: true
          description: Staff user who owns this issue's triage, when assigned.
        assigned_to_email:
          type: string
          format: email
          readOnly: true
        resolution_note:
          type: string
          description: Free-form note set when the issue moves to resolved/wontfix.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - assigned_to_email
        - client_uid
        - context
        - created
        - id
        - importer
        - importer_version
        - kind
        - modified
        - occurred_at
        - pms
        - pms_version
        - practice_uid
        - summary
    ImportIssueList:
      type: object
      description: |-
        Compact list shape for the inbox.

        ``context`` is intentionally omitted -- the inbox list view
        typically renders dozens of rows at a time and the JSON payload
        can carry many KB of stack-trace / row-dump per row.  The detail
        serializer surfaces ``context`` when the support user opens a
        single issue.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        importer:
          type: string
          readOnly: true
          description: Slug of the reporting importer, e.g. 'escape-eaglesoft'.
        importer_version:
          type: string
          readOnly: true
          description: Self-reported version of the importer that raised the issue.
        pms:
          type: string
          readOnly: true
          description: Slug of the upstream practice management system.
        pms_version:
          type: string
          readOnly: true
          description: Self-reported PMS version, when the importer can read it.
        practice_uid:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Practice UUID the importer was syncing, when established.
        client_uid:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Client (parent of practice) UUID, when established.
        kind:
          allOf:
            - $ref: '#/components/schemas/KindEnum'
          readOnly: true
          description: |-
            Failure class (schema change / data shape / runtime).

            * `schema_change` - Schema change
            * `data_shape` - Data shape surprise
            * `runtime` - Runtime error
        occurred_at:
          type: string
          format: date-time
          readOnly: true
          description: Timestamp the importer recorded the error (importer's clock).
        summary:
          type: string
          readOnly: true
          description: One-line description for the inbox list view (PHI-free).
        status:
          allOf:
            - $ref: '#/components/schemas/ImportIssueStatusEnum'
          readOnly: true
          description: |-
            Triage state.

            * `new` - New
            * `triaged` - Triaged
            * `resolved` - Resolved
            * `wontfix` - Won't fix
        assigned_to:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff user who owns this issue's triage, when assigned.
        assigned_to_email:
          type: string
          format: email
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - assigned_to
        - assigned_to_email
        - client_uid
        - created
        - id
        - importer
        - importer_version
        - kind
        - modified
        - occurred_at
        - pms
        - pms_version
        - practice_uid
        - status
        - summary
    ImportIssueStatusEnum:
      enum:
        - new
        - triaged
        - resolved
        - wontfix
      type: string
      description: |-
        * `new` - New
        * `triaged` - Triaged
        * `resolved` - Resolved
        * `wontfix` - Won't fix
    ImportSchemaRequest:
      type: object
      description: Payload for uploading a source PMS schema dump.
      properties:
        schema_text:
          type: string
          minLength: 1
        source_type:
          type: string
          minLength: 1
          maxLength: 20
        metadata:
          type: object
          additionalProperties: {}
      required:
        - schema_text
        - source_type
    ImportSchemaUploadResponse:
      type: object
      description: Response from ``POST /sync/sessions/<id>/schema/``.
      properties:
        detail:
          type: string
      required:
        - detail
    ImportSessionCompleteRequest:
      type: object
      description: Payload for completing/finalizing an import session.
      properties:
        session_id:
          type: string
          format: uuid
        results:
          type: array
          items:
            type: object
            additionalProperties: {}
        report:
          type: object
          additionalProperties: {}
    ImportSessionCompleteResponse:
      type: object
      description: Response from ``POST /sync/sessions/<id>/complete/``.
      properties:
        session_id:
          type: string
        status:
          type: string
        completed_at:
          type: string
        record_counts:
          type: object
          additionalProperties:
            type: integer
      required:
        - completed_at
        - record_counts
        - session_id
        - status
    ImportSessionConvertResponse:
      type: object
      description: Response from ``POST /sync/sessions/<id>/convert/``.
      properties:
        detail:
          type: string
        session_id:
          type: string
      required:
        - detail
        - session_id
    ImportSessionCreateRequest:
      type: object
      description: Payload for creating an import session.
      properties:
        source:
          $ref: '#/components/schemas/ImportSessionCreateSourceEnum'
      required:
        - source
    ImportSessionCreateResponse:
      type: object
      description: Response from ``POST /sync/sessions/``.
      properties:
        session_id:
          type: string
        practice_id:
          type: integer
        practice_name:
          type: string
      required:
        - practice_id
        - practice_name
        - session_id
    ImportSessionCreateSourceEnum:
      enum:
        - opendental
        - eaglesoft
        - dentrix
        - restore
        - faker
      type: string
      description: |-
        * `opendental` - OpenDental
        * `eaglesoft` - Eaglesoft
        * `dentrix` - Dentrix
        * `restore` - Backup Restore
        * `faker` - Demo Data
    ImportSessionDetailResponse:
      type: object
      description: Response from ``GET /sync/sessions/<id>/``.
      properties:
        id:
          type: string
        source:
          type: string
        status:
          type: string
        started_at:
          type: string
        completed_at:
          type: string
          nullable: true
        created_by:
          type: string
          format: email
          nullable: true
        record_counts:
          type: object
          additionalProperties:
            type: integer
        total_records:
          type: integer
        conversion_error_count:
          type: integer
        import_report:
          type: object
          additionalProperties: {}
        import_error_details:
          type: array
          items:
            type: object
            additionalProperties: {}
        has_schema:
          type: boolean
        conversion:
          $ref: '#/components/schemas/ConversionProgressSummary'
      required:
        - completed_at
        - created_by
        - has_schema
        - id
        - record_counts
        - source
        - started_at
        - status
        - total_records
    ImportSessionListItem:
      type: object
      description: One entry in the response from ``GET /sync/sessions/``.
      properties:
        id:
          type: string
        source:
          type: string
        status:
          type: string
        started_at:
          type: string
        completed_at:
          type: string
          nullable: true
        created_by:
          type: string
          format: email
          nullable: true
        record_counts:
          type: object
          additionalProperties:
            type: integer
        total_records:
          type: integer
      required:
        - completed_at
        - created_by
        - id
        - record_counts
        - source
        - started_at
        - status
        - total_records
    InboxItem:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        practice:
          type: string
          format: uuid
          readOnly: true
        original_filename:
          type: string
          readOnly: true
        mime_type:
          type: string
          readOnly: true
        file_size:
          type: integer
          readOnly: true
          description: File size in bytes
        title:
          type: string
          maxLength: 255
        notes:
          type: string
          description: Staff notes during triage
        source:
          allOf:
            - $ref: '#/components/schemas/InboxItemSourceEnum'
          readOnly: true
        inbound_email:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: The inbound email that created this item (scan-to-email only)
        uploaded_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        source_application:
          type: integer
          readOnly: true
          nullable: true
          description: OAuth2 application that uploaded this item
        expires_at:
          type: string
          format: date-time
          readOnly: true
          description: Auto-purge after this date
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        download_url:
          type: string
          readOnly: true
        uploaded_by_name:
          type: string
          readOnly: true
        source_application_name:
          type: string
          readOnly: true
        custom_data: {}
      required:
        - created
        - download_url
        - expires_at
        - file_size
        - id
        - inbound_email
        - mime_type
        - modified
        - original_filename
        - practice
        - source
        - source_application
        - source_application_name
        - uploaded_by
        - uploaded_by_name
    InboxItemRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        title:
          type: string
          maxLength: 255
        notes:
          type: string
          description: Staff notes during triage
        custom_data: {}
    InboxItemSourceEnum:
      enum:
        - web
        - scan_email
        - api
      type: string
      description: |-
        * `web` - Web Upload
        * `scan_email` - Scan to Email
        * `api` - API Upload
    InstalledApp:
      type: object
      description: |-
        Public response shape for ``GET /v1/portal/installed-apps/?slot=...``.

        Hand-rolled (not a ``ModelSerializer``) because the payload stitches
        together fields from :class:`oauth2_provider.Application` and its
        :class:`apps.oauth.models.OAuthAppProfile` one-to-one row.  See
        :mod:`apps.portal.views_installed_apps` for the URL-template
        substitution contract — substitution happens client-side at click
        time, so this view does not need patient or practice context.
      properties:
        client_id:
          type: string
          description: Public OAuth client identifier.
        name:
          type: string
          description: Human-readable application name.
        icon_url:
          type: string
          description: >-
            Best-available URL for the app's logo. Falls through uploaded logo →
            legacy logo_url → empty string.
        url_template:
          type: string
          description: >-
            URL template the web client opens on click. May contain {patient_id}
            and {practice_id} placeholders. Substitution is the client's
            responsibility.
      required:
        - client_id
        - icon_url
        - name
        - url_template
    InsuranceBenefit:
      type: object
      description: |-
        Read-only serializer for insurance benefit details.

        Exposes both the legacy ``proc_code`` string (import fallback) and a
        nested ``procedure_code`` object sourced from the new optional FK.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        custom_data:
          readOnly: true
        procedure_code:
          allOf:
            - $ref: '#/components/schemas/ProcedureCodeNested'
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        benefit_type:
          readOnly: true
          oneOf:
            - $ref: '#/components/schemas/BenefitTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        coverage_category:
          readOnly: true
          oneOf:
            - $ref: '#/components/schemas/CoverageCategoryEnum'
            - $ref: '#/components/schemas/BlankEnum'
        percent:
          type: string
          format: decimal
          pattern: ^-?\d{0,3}(?:\.\d{0,2})?$
          readOnly: true
        monetary_amt:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        time_period:
          readOnly: true
          oneOf:
            - $ref: '#/components/schemas/TimePeriodEnum'
            - $ref: '#/components/schemas/BlankEnum'
        proc_code:
          type: string
          readOnly: true
          description: Benefit tied to a specific procedure code
          maxLength: 20
        min_age:
          type: integer
          maximum: 32767
          minimum: 0
          readOnly: true
          nullable: true
          description: Minimum patient age (inclusive) for this benefit to apply
        max_age:
          type: integer
          maximum: 32767
          minimum: 0
          readOnly: true
          nullable: true
          description: Maximum patient age (inclusive) for this benefit to apply
        waiting_period_months:
          type: integer
          maximum: 32767
          minimum: 0
          readOnly: true
          nullable: true
          description: Months from effective date before this benefit is available
        created_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        modified_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        practice:
          type: string
          format: uuid
          readOnly: true
        insurance_plan:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        patient_insurance:
          type: string
          format: uuid
          readOnly: true
          nullable: true
      required:
        - benefit_type
        - coverage_category
        - created
        - created_by
        - custom_data
        - id
        - insurance_plan
        - max_age
        - min_age
        - modified
        - modified_by
        - monetary_amt
        - patient_insurance
        - percent
        - practice
        - proc_code
        - procedure_code
        - time_period
        - waiting_period_months
    InsuranceBenefitWrite:
      type: object
      description: |-
        Write serializer for insurance benefits.

        The ``procedure_code`` FK is accepted as a write-only PK; responses
        should use :class:`InsuranceBenefitSerializer` to render the nested
        object.  The legacy ``proc_code`` string remains accepted as-is for
        PMS-importer compatibility.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        insurance_plan:
          type: string
          format: uuid
          nullable: true
        benefit_type:
          oneOf:
            - $ref: '#/components/schemas/BenefitTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        coverage_category:
          oneOf:
            - $ref: '#/components/schemas/CoverageCategoryEnum'
            - $ref: '#/components/schemas/BlankEnum'
        percent:
          type: string
          format: decimal
          pattern: ^-?\d{0,3}(?:\.\d{0,2})?$
        monetary_amt:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        time_period:
          oneOf:
            - $ref: '#/components/schemas/TimePeriodEnum'
            - $ref: '#/components/schemas/BlankEnum'
        proc_code:
          type: string
          description: Benefit tied to a specific procedure code
          maxLength: 20
        procedure_code:
          type: string
          format: uuid
          nullable: true
        min_age:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Minimum patient age (inclusive) for this benefit to apply
        max_age:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Maximum patient age (inclusive) for this benefit to apply
        waiting_period_months:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Months from effective date before this benefit is available
        custom_data: {}
      required:
        - id
    InsuranceBenefitWriteRequest:
      type: object
      description: |-
        Write serializer for insurance benefits.

        The ``procedure_code`` FK is accepted as a write-only PK; responses
        should use :class:`InsuranceBenefitSerializer` to render the nested
        object.  The legacy ``proc_code`` string remains accepted as-is for
        PMS-importer compatibility.
      properties:
        insurance_plan:
          type: string
          format: uuid
          nullable: true
        benefit_type:
          oneOf:
            - $ref: '#/components/schemas/BenefitTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        coverage_category:
          oneOf:
            - $ref: '#/components/schemas/CoverageCategoryEnum'
            - $ref: '#/components/schemas/BlankEnum'
        percent:
          type: string
          format: decimal
          pattern: ^-?\d{0,3}(?:\.\d{0,2})?$
        monetary_amt:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        time_period:
          oneOf:
            - $ref: '#/components/schemas/TimePeriodEnum'
            - $ref: '#/components/schemas/BlankEnum'
        proc_code:
          type: string
          description: Benefit tied to a specific procedure code
          maxLength: 20
        procedure_code:
          type: string
          format: uuid
          nullable: true
        min_age:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Minimum patient age (inclusive) for this benefit to apply
        max_age:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Maximum patient age (inclusive) for this benefit to apply
        waiting_period_months:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Months from effective date before this benefit is available
        custom_data: {}
    InsuranceCarrier:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          maxLength: 255
        phone:
          type: string
          maxLength: 20
        website:
          type: string
          format: uri
          maxLength: 200
        address_line1:
          type: string
          maxLength: 255
        address_line2:
          type: string
          maxLength: 255
        city:
          type: string
          maxLength: 100
        state:
          type: string
          maxLength: 2
        zip_code:
          type: string
          maxLength: 10
        payer_id:
          type: string
          description: EDI payer ID for clearinghouse submission
          maxLength: 50
        claim_office_id:
          type: string
          description: Separate claim submission office ID
          maxLength: 50
        hpid:
          type: string
          description: Health Plan Identifier
          maxLength: 50
        payment_source:
          type: string
          description: check, eft, virtual_card
          maxLength: 50
        notes:
          type: string
        is_active:
          type: boolean
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - created
        - id
        - modified
        - name
    InsuranceCarrierRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        phone:
          type: string
          maxLength: 20
        website:
          type: string
          format: uri
          maxLength: 200
        address_line1:
          type: string
          maxLength: 255
        address_line2:
          type: string
          maxLength: 255
        city:
          type: string
          maxLength: 100
        state:
          type: string
          maxLength: 2
        zip_code:
          type: string
          maxLength: 10
        payer_id:
          type: string
          description: EDI payer ID for clearinghouse submission
          maxLength: 50
        claim_office_id:
          type: string
          description: Separate claim submission office ID
          maxLength: 50
        hpid:
          type: string
          description: Health Plan Identifier
          maxLength: 50
        payment_source:
          type: string
          description: check, eft, virtual_card
          maxLength: 50
        notes:
          type: string
        is_active:
          type: boolean
        custom_data: {}
      required:
        - name
    InsuranceClaim:
      type: object
      description: >-
        Read-only serializer for insurance claims with nested procedures and
        status log.


        Exposes pre-joined display fields so the practice-wide claims list page

        (``/insurance/claims``) can render patient and provider names without a

        second round-trip (api#470).  ``PracticeClaimListView.get_queryset``

        already does ``.select_related("patient", "provider")``, so these

        ``SerializerMethodField`` additions cost zero extra queries.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        custom_data:
          readOnly: true
        procedures:
          type: array
          items:
            $ref: '#/components/schemas/ClaimProcedure'
          readOnly: true
        status_log:
          type: array
          items:
            $ref: '#/components/schemas/ClaimStatusLog'
          readOnly: true
        patient_name:
          type: string
          nullable: true
          description: Return ``"Last, First"`` for the claim's patient, or ``None``.
          readOnly: true
        patient_first_name:
          type: string
          nullable: true
          readOnly: true
        patient_last_name:
          type: string
          nullable: true
          readOnly: true
        provider_name:
          type: string
          nullable: true
          description: Return ``"Last, First"`` for the rendering provider, or ``None``.
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        date_service:
          type: string
          format: date
          readOnly: true
          nullable: true
        date_sent:
          type: string
          format: date
          readOnly: true
          nullable: true
        status:
          allOf:
            - $ref: '#/components/schemas/ClaimStatusEnum'
          readOnly: true
          description: |-
            Controlled claim status (state machine enforced)

            * `draft` - Draft
            * `submitted` - Submitted
            * `accepted` - Accepted
            * `rejected` - Rejected
            * `resubmitted` - Resubmitted
            * `paid` - Paid
            * `partial` - Partially Paid
            * `denied` - Denied
            * `appealed` - Appealed
            * `void` - Void
            * `imported` - Imported
        claim_status:
          type: string
          readOnly: true
          description: Legacy free-text status from PMS imports
          maxLength: 50
        claim_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        ins_pay_est:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        ins_pay_amt:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        ded_applied:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        write_off:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        created_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        modified_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        practice:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        provider:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        patient_insurance:
          type: string
          format: uuid
          readOnly: true
          nullable: true
      required:
        - claim_fee
        - claim_status
        - created
        - created_by
        - custom_data
        - date_sent
        - date_service
        - ded_applied
        - id
        - ins_pay_amt
        - ins_pay_est
        - modified
        - modified_by
        - patient
        - patient_first_name
        - patient_insurance
        - patient_last_name
        - patient_name
        - practice
        - procedures
        - provider
        - provider_name
        - status
        - status_log
        - write_off
    InsuranceClaimWrite:
      type: object
      description: >-
        Serializer for creating and updating insurance claims.


        Supports nested creation of claim procedures via the ``procedures``
        field.

        The ``practice`` and ``status`` fields are set by the view, not the
        client.


        GH-711: when the bulk-array path injects ``patient`` via

        :func:`apps.sync.views_bulk.make_bulk_serializer`, that injected FK plus

        the ``provider`` and ``patient_insurance`` FKs are checked against the

        URL practice.  The staff path's ``patient`` is read-only and set by

        the view from the URL — the validator no-ops in that case since the

        attribute is absent from ``validated_data``.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        provider:
          type: string
          format: uuid
          nullable: true
        patient_insurance:
          type: string
          format: uuid
          nullable: true
        date_service:
          type: string
          format: date
          nullable: true
        date_sent:
          type: string
          format: date
          nullable: true
        claim_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        ins_pay_est:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        ins_pay_amt:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        ded_applied:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        write_off:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        procedures:
          type: array
          items:
            $ref: '#/components/schemas/ClaimProcedureWrite'
        custom_data: {}
      required:
        - id
        - patient
    InsuranceClaimWriteRequest:
      type: object
      description: >-
        Serializer for creating and updating insurance claims.


        Supports nested creation of claim procedures via the ``procedures``
        field.

        The ``practice`` and ``status`` fields are set by the view, not the
        client.


        GH-711: when the bulk-array path injects ``patient`` via

        :func:`apps.sync.views_bulk.make_bulk_serializer`, that injected FK plus

        the ``provider`` and ``patient_insurance`` FKs are checked against the

        URL practice.  The staff path's ``patient`` is read-only and set by

        the view from the URL — the validator no-ops in that case since the

        attribute is absent from ``validated_data``.
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        patient_insurance:
          type: string
          format: uuid
          nullable: true
        date_service:
          type: string
          format: date
          nullable: true
        date_sent:
          type: string
          format: date
          nullable: true
        claim_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        ins_pay_est:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        ins_pay_amt:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        ded_applied:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        write_off:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        procedures:
          type: array
          items:
            $ref: '#/components/schemas/ClaimProcedureWriteRequest'
        custom_data: {}
    InsurancePlan:
      type: object
      description: >-
        Read/write serializer for insurance plans.


        Supports nested ``benefits`` on create and update.  On POST/PATCH the

        serializer accepts a list of benefit payloads (same shape as

        :class:`InsuranceBenefitWriteSerializer`) and materializes them in a

        single ``bulk_create`` keyed to the parent plan's practice.


        GH-711: ``carrier`` and ``fee_schedule`` FKs must belong to URL
        practice.


        Update semantics mirror :class:`TreatmentPlanWriteSerializer`:


        - If ``benefits`` is present in the payload, it fully replaces the
          plan's existing benefits (snapshot-replace).  Once a practice has
          migrated off a legacy PMS the Escape API is the source of truth, so
          sync tools and UI clients that want to update the benefit list send
          the complete new list.
        - If ``benefits`` is omitted, the plan's existing benefits are left
          untouched — callers patching plan-level fields don't need to ship
          the full benefits collection.

        Output shape is unchanged: benefits are always rendered with

        :class:`InsuranceBenefitSerializer` so the nested ``procedure_code``

        object is included.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        carrier:
          type: string
          format: uuid
          nullable: true
        carrier_name:
          type: string
          readOnly: true
        plan_name:
          type: string
          maxLength: 255
        group_name:
          type: string
          maxLength: 255
        group_number:
          type: string
          maxLength: 100
        employer_name:
          type: string
          maxLength: 255
        plan_type:
          oneOf:
            - $ref: '#/components/schemas/PlanTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        annual_maximum:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
        individual_deductible:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
        family_deductible:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
        plan_year_begin_month:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Month (1-12) when the plan year resets
        fee_schedule:
          type: string
          format: uuid
          nullable: true
          description: Carrier-specific fee schedule (allowed amounts) for this plan
        notes:
          type: string
        is_active:
          type: boolean
        benefits:
          type: array
          items:
            $ref: '#/components/schemas/InsuranceBenefitWrite'
        enrollment_count:
          type: integer
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - carrier_name
        - created
        - enrollment_count
        - id
        - modified
    InsurancePlanList:
      type: object
      description: Lighter serializer for plan list views — no nested benefits.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        carrier:
          type: string
          format: uuid
          nullable: true
        carrier_name:
          type: string
          readOnly: true
        plan_name:
          type: string
          maxLength: 255
        group_name:
          type: string
          maxLength: 255
        group_number:
          type: string
          maxLength: 100
        employer_name:
          type: string
          maxLength: 255
        plan_type:
          oneOf:
            - $ref: '#/components/schemas/PlanTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        annual_maximum:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
        individual_deductible:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
        family_deductible:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
        plan_year_begin_month:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Month (1-12) when the plan year resets
        fee_schedule:
          type: string
          format: uuid
          nullable: true
          description: Carrier-specific fee schedule (allowed amounts) for this plan
        is_active:
          type: boolean
        enrollment_count:
          type: integer
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - carrier_name
        - created
        - enrollment_count
        - id
        - modified
    InsurancePlanRequest:
      type: object
      description: >-
        Read/write serializer for insurance plans.


        Supports nested ``benefits`` on create and update.  On POST/PATCH the

        serializer accepts a list of benefit payloads (same shape as

        :class:`InsuranceBenefitWriteSerializer`) and materializes them in a

        single ``bulk_create`` keyed to the parent plan's practice.


        GH-711: ``carrier`` and ``fee_schedule`` FKs must belong to URL
        practice.


        Update semantics mirror :class:`TreatmentPlanWriteSerializer`:


        - If ``benefits`` is present in the payload, it fully replaces the
          plan's existing benefits (snapshot-replace).  Once a practice has
          migrated off a legacy PMS the Escape API is the source of truth, so
          sync tools and UI clients that want to update the benefit list send
          the complete new list.
        - If ``benefits`` is omitted, the plan's existing benefits are left
          untouched — callers patching plan-level fields don't need to ship
          the full benefits collection.

        Output shape is unchanged: benefits are always rendered with

        :class:`InsuranceBenefitSerializer` so the nested ``procedure_code``

        object is included.
      properties:
        carrier:
          type: string
          format: uuid
          nullable: true
        plan_name:
          type: string
          maxLength: 255
        group_name:
          type: string
          maxLength: 255
        group_number:
          type: string
          maxLength: 100
        employer_name:
          type: string
          maxLength: 255
        plan_type:
          oneOf:
            - $ref: '#/components/schemas/PlanTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        annual_maximum:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
        individual_deductible:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
        family_deductible:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
        plan_year_begin_month:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Month (1-12) when the plan year resets
        fee_schedule:
          type: string
          format: uuid
          nullable: true
          description: Carrier-specific fee schedule (allowed amounts) for this plan
        notes:
          type: string
        is_active:
          type: boolean
        benefits:
          type: array
          items:
            $ref: '#/components/schemas/InsuranceBenefitWriteRequest'
        custom_data: {}
    InsuranceVerification:
      type: object
      description: |-
        Read/create serializer for :class:`InsuranceVerification`.

        Nested ``plan``/``patient``/``verified_by`` summaries minimize round
        trips on list responses.  ``source`` is validated against
        :class:`VerificationSource`; empty structured fields stay ``None``
        rather than coercing to zero so reports can distinguish "not quoted"
        from "quoted as $0".
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
        insurance_plan:
          type: string
          format: uuid
        verified_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff member who captured the verification.
        verified_at:
          type: string
          format: date-time
          description: When the verification was obtained from the payer / portal.
        source:
          $ref: '#/components/schemas/InsuranceVerificationSourceEnum'
        raw_response:
          description: Raw payer/clearinghouse/portal blob (270/271, screen scrape, etc).
        annual_max:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Annual maximum benefit in dollars, if quoted.  NULL = not provided.
        deductible_total:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Total deductible for the plan year, if quoted.  NULL = not provided.
        deductible_met:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: >-
            Portion of the deductible already met, if quoted.  NULL = not
            provided.
        waiting_period_notes:
          type: string
          description: >-
            Free-text notes on waiting periods (e.g. 'major services 6mo WP
            remaining').
        frequency_limits:
          description: >-
            Structured frequency limitations, e.g. {'D1110': '2 per calendar
            year'}.  Defaults to empty dict so callers can merge without
            None-checks.
        plan:
          allOf:
            - $ref: '#/components/schemas/_InsurancePlanVerificationSummary'
          readOnly: true
        patient_summary:
          allOf:
            - $ref: '#/components/schemas/_PatientVerificationSummary'
          readOnly: true
        verified_by_summary:
          allOf:
            - $ref: '#/components/schemas/_VerifiedBySummary'
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - id
        - insurance_plan
        - modified
        - patient
        - patient_summary
        - plan
        - source
        - verified_at
        - verified_by
        - verified_by_summary
    InsuranceVerificationRequest:
      type: object
      description: |-
        Read/create serializer for :class:`InsuranceVerification`.

        Nested ``plan``/``patient``/``verified_by`` summaries minimize round
        trips on list responses.  ``source`` is validated against
        :class:`VerificationSource`; empty structured fields stay ``None``
        rather than coercing to zero so reports can distinguish "not quoted"
        from "quoted as $0".
      properties:
        patient:
          type: string
          format: uuid
        insurance_plan:
          type: string
          format: uuid
        verified_at:
          type: string
          format: date-time
          description: When the verification was obtained from the payer / portal.
        source:
          $ref: '#/components/schemas/InsuranceVerificationSourceEnum'
        raw_response:
          description: Raw payer/clearinghouse/portal blob (270/271, screen scrape, etc).
        annual_max:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Annual maximum benefit in dollars, if quoted.  NULL = not provided.
        deductible_total:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Total deductible for the plan year, if quoted.  NULL = not provided.
        deductible_met:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: >-
            Portion of the deductible already met, if quoted.  NULL = not
            provided.
        waiting_period_notes:
          type: string
          description: >-
            Free-text notes on waiting periods (e.g. 'major services 6mo WP
            remaining').
        frequency_limits:
          description: >-
            Structured frequency limitations, e.g. {'D1110': '2 per calendar
            year'}.  Defaults to empty dict so callers can merge without
            None-checks.
      required:
        - insurance_plan
        - patient
        - source
        - verified_at
    InsuranceVerificationSourceEnum:
      enum:
        - phone
        - portal
        - clearinghouse
      type: string
      description: |-
        * `phone` - Phone
        * `portal` - Portal
        * `clearinghouse` - Clearinghouse
    InviteCode:
      type: object
      description: |-
        Read serializer for invite-code list responses.

        Surfaces the live :meth:`InviteCode.status` value rather than any
        individual column so consumers don't have to re-derive the state.
        ``redeemed_by_client_name`` is the redeemer's display name -- null
        until the code is consumed -- so the staff UI can show "redeemed by
        Foo Dental" without an extra round-trip.

        ``client`` (the issuing Client's UID) and ``created_by_email`` are
        exposed read-only so the support app can distinguish staff-bulk
        codes (``client is None``) from client-minted ones and attribute
        each row to the staff member who minted it.  Both are nullable:
        ``client`` is null for staff-bulk codes and ``created_by_email``
        is null for system-generated (auto-mint) codes.
      properties:
        code:
          type: string
          readOnly: true
          description: >-
            Human-readable invite code.  Generated from a 27-char base32
            alphabet with visually-confusing characters removed (no 0/O/1/I/L).
        status:
          type: string
          readOnly: true
        client:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        created_by_email:
          type: string
          format: email
          nullable: true
          readOnly: true
        created_at:
          type: string
          format: date-time
          readOnly: true
        expires_at:
          type: string
          format: date-time
          readOnly: true
          description: >-
            When this code stops being redeemable.  Defaults to created_at + 90
            days.
        redeemed_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        redeemed_by_client_name:
          type: string
          nullable: true
          readOnly: true
      required:
        - client
        - code
        - created_at
        - created_by_email
        - expires_at
        - redeemed_at
        - redeemed_by_client_name
        - status
    InviteCodeCreateRequest:
      type: object
      description: |-
        Write serializer for staff-only invite-code minting.

        ``client`` is optional -- staff can mint a bulk code that any
        sign-up can redeem by leaving it null.  When supplied, the value is
        a Client UID (UUID) that already exists.
      properties:
        client:
          type: string
          format: uuid
          nullable: true
    InviteCodeMintResponse:
      type: object
      description: Response shape for ``POST /api/v1/accounts/invite-codes/``.
      properties:
        code:
          type: string
          readOnly: true
          description: >-
            Human-readable invite code.  Generated from a 27-char base32
            alphabet with visually-confusing characters removed (no 0/O/1/I/L).
        expires_at:
          type: string
          format: date-time
          readOnly: true
          description: >-
            When this code stops being redeemable.  Defaults to created_at + 90
            days.
        created_at:
          type: string
          format: date-time
          readOnly: true
      required:
        - code
        - created_at
        - expires_at
    InviteCodeValidateResponse:
      type: object
      description: |-
        Response shape for the public ``GET /invite-codes/validate/`` endpoint.

        Carries no Client identity -- per the spec the validation endpoint
        must not leak who issued or owns the code.
      properties:
        valid:
          type: boolean
        error:
          type: string
      required:
        - valid
    KindEnum:
      enum:
        - schema_change
        - data_shape
        - runtime
      type: string
      description: |-
        * `schema_change` - Schema change
        * `data_shape` - Data shape surprise
        * `runtime` - Runtime error
    KioskArrivePatient:
      type: object
      description: |-
        Lite patient projection returned to the kiosk on a successful arrival.

        Mirrors :class:`PatientIntakeLookupView`'s row shape so the kiosk
        UI can reuse the same render path it uses for the multi-call
        fallback.  PHI is intentionally minimal — first/last name and DOB
        are the only fields the kiosk actually needs to confirm the match
        on-screen.
      properties:
        id:
          type: string
          format: uuid
        first_name:
          type: string
        last_name:
          type: string
        date_of_birth:
          type: string
          format: date
          nullable: true
        phone_mobile:
          type: string
      required:
        - date_of_birth
        - first_name
        - id
        - last_name
        - phone_mobile
    KioskArriveRequestRequest:
      type: object
      description: Request body for the single-call kiosk arrival endpoint.
      properties:
        last_name:
          type: string
          minLength: 1
          description: Patient's last name (case-insensitive exact match).
          maxLength: 100
        date_of_birth:
          type: string
          format: date
          description: Patient's date of birth (``YYYY-MM-DD``).
      required:
        - date_of_birth
        - last_name
    KioskArriveResponse:
      type: object
      description: |-
        Full success-shape envelope for the single-call kiosk arrival.

        The ``appointment`` payload is the standard scheduling
        :class:`apps.scheduling.serializers.AppointmentSerializer` shape so
        the kiosk's transition fallback can interchange between this
        endpoint and the staff ``/transition/`` endpoint without remapping.
      properties:
        patient:
          $ref: '#/components/schemas/KioskArrivePatient'
        appointment:
          type: object
          additionalProperties: {}
          description: Appointment payload — see ``AppointmentSerializer``.
      required:
        - appointment
        - patient
    KioskCheckInRequestRequest:
      type: object
      description: |-
        Request body for the kiosk check-in endpoint.

        Both fields are required.  ``last_name`` is matched case-insensitive
        in the view; we keep the raw value here so a stray
        ``strip``/``lower`` on the serializer side does not silently change
        the comparison surface (the view owns the predicate).
      properties:
        last_name:
          type: string
          minLength: 1
          maxLength: 100
        dob:
          type: string
          format: date
          description: Patient date of birth, ISO-8601 (YYYY-MM-DD).
      required:
        - dob
        - last_name
    KioskCheckInResponse:
      type: object
      description: |-
        HIPAA-safe response body.

        No patient identifiers.  ``scheduled_at`` is the appointment's UTC
        start (the project-wide API contract; the kiosk converts to the
        practice tz for display if it wants to).
      properties:
        appointment_uid:
          type: string
          format: uuid
        scheduled_at:
          type: string
          format: date-time
        checked_in_at:
          type: string
          format: date-time
      required:
        - appointment_uid
        - checked_in_at
        - scheduled_at
    KioskSessionCreate:
      type: object
      description: |-
        Staff-side create for a new kiosk session.

        Only ``device_label`` and ``idle_timeout_seconds`` are writable;
        everything else (token, started_at, etc.) is server-generated.
        The ``kiosk_url`` read-only field is composed at response time so
        the staff client gets a ready-to-load URL without a second round
        trip.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        device_label:
          type: string
          description: Optional human-readable label, e.g. 'Front desk tablet 1'.
          maxLength: 120
        idle_timeout_seconds:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: >-
            Seconds of inactivity after which the session is considered expired
            (default 10 minutes).
        token:
          type: string
          format: uuid
          readOnly: true
          description: >-
            Opaque UUID threaded into the kiosk URL the device loads. NOT
            patient-bound; rotating the row generates a fresh token.
        started_at:
          type: string
          format: date-time
          readOnly: true
          description: When the staff member first created this kiosk session.
        last_activity_at:
          type: string
          format: date-time
          readOnly: true
          description: >-
            Updated on every kiosk POST (load-sendout, public state read, public
            submit). Used together with idle_timeout_seconds to decide whether
            the session is still active.
        ended_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: Set when staff explicitly ends the session via /end/.
        current_sendout:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: >-
            Set when staff loads a specific sendout onto the kiosk. Cleared when
            the patient finishes or staff ends the session. SET_NULL so deleting
            the source sendout doesn't cascade-kill the audit trail of which
            kiosks served it.
        kiosk_url:
          type: string
          readOnly: true
      required:
        - current_sendout
        - ended_at
        - id
        - kiosk_url
        - last_activity_at
        - started_at
        - token
    KioskSessionCreateRequest:
      type: object
      description: |-
        Staff-side create for a new kiosk session.

        Only ``device_label`` and ``idle_timeout_seconds`` are writable;
        everything else (token, started_at, etc.) is server-generated.
        The ``kiosk_url`` read-only field is composed at response time so
        the staff client gets a ready-to-load URL without a second round
        trip.
      properties:
        device_label:
          type: string
          description: Optional human-readable label, e.g. 'Front desk tablet 1'.
          maxLength: 120
        idle_timeout_seconds:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: >-
            Seconds of inactivity after which the session is considered expired
            (default 10 minutes).
    KioskSessionDetail:
      type: object
      description: |-
        Staff-side retrieve serializer — adds ``is_active`` for the UI.

        The detail surface is read-only; ``current_sendout`` is mutated via
        the dedicated ``/load-sendout/`` action so the view layer can run
        the practice-scope check on the sendout reference.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        device_label:
          type: string
          readOnly: true
          description: Optional human-readable label, e.g. 'Front desk tablet 1'.
        idle_timeout_seconds:
          type: integer
          readOnly: true
          description: >-
            Seconds of inactivity after which the session is considered expired
            (default 10 minutes).
        token:
          type: string
          format: uuid
          readOnly: true
          description: >-
            Opaque UUID threaded into the kiosk URL the device loads. NOT
            patient-bound; rotating the row generates a fresh token.
        started_at:
          type: string
          format: date-time
          readOnly: true
          description: When the staff member first created this kiosk session.
        last_activity_at:
          type: string
          format: date-time
          readOnly: true
          description: >-
            Updated on every kiosk POST (load-sendout, public state read, public
            submit). Used together with idle_timeout_seconds to decide whether
            the session is still active.
        ended_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: Set when staff explicitly ends the session via /end/.
        is_active:
          type: boolean
          readOnly: true
        current_sendout:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: >-
            Set when staff loads a specific sendout onto the kiosk. Cleared when
            the patient finishes or staff ends the session. SET_NULL so deleting
            the source sendout doesn't cascade-kill the audit trail of which
            kiosks served it.
        kiosk_url:
          type: string
          readOnly: true
      required:
        - current_sendout
        - device_label
        - ended_at
        - id
        - idle_timeout_seconds
        - is_active
        - kiosk_url
        - last_activity_at
        - started_at
        - token
    LogoUploadRequest:
      type: object
      description: |-
        Accepts a single image file for logo upload.

        Validation of MIME type, file size, and dimensions is handled
        downstream by ``process_logo_image()``.
      properties:
        file:
          type: string
          format: binary
      required:
        - file
    LogoUrlResponse:
      type: object
      properties:
        logo_url:
          type: string
          format: uri
      required:
        - logo_url
    MemberRoleEnum:
      enum:
        - ADMIN
        - OFFICE_MANAGER
        - DENTIST
        - HYGIENIST
        - FRONT_DESK
        - BILLING
        - READ_ONLY
        - IT_SUPPORT
      type: string
      description: |-
        * `ADMIN` - Administrator
        * `OFFICE_MANAGER` - Office Manager
        * `DENTIST` - Dentist
        * `HYGIENIST` - Hygienist
        * `FRONT_DESK` - Front Desk
        * `BILLING` - Billing
        * `READ_ONLY` - Read Only
        * `IT_SUPPORT` - IT Support
    Message:
      type: object
      description: Read serializer for a single message.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        sender_id:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        sender_name:
          type: string
          nullable: true
          readOnly: true
        body:
          type: string
          readOnly: true
        is_system:
          type: boolean
          readOnly: true
          description: True for auto-generated notifications.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - body
        - created
        - id
        - is_system
        - sender_id
        - sender_name
    ModeEnum:
      enum:
        - phone
        - email
        - text
        - fax
        - mail
        - in_person
        - web
        - other
      type: string
      description: |-
        * `phone` - Phone
        * `email` - Email
        * `text` - Text
        * `fax` - Fax
        * `mail` - Mail
        * `in_person` - In Person
        * `web` - Web
        * `other` - Other
    MonthSummaryDay:
      type: object
      properties:
        appointment_count:
          type: integer
        production:
          type: string
          description: Decimal string, 2 places.
      required:
        - appointment_count
        - production
    MonthSummaryResponse:
      type: object
      properties:
        from:
          type: string
          format: date
        to:
          type: string
          format: date
        days:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/MonthSummaryDay'
      required:
        - days
        - from
        - to
    MountTemplate:
      type: object
      description: Read-only view of system mount templates (FMX-18, BW-4, etc.).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          readOnly: true
        code:
          type: string
          readOnly: true
          description: Stable identifier, e.g. 'fmx-18', 'bw-4', 'pano'.
        description:
          type: string
          readOnly: true
        slot_layout:
          readOnly: true
          description: >-
            Describes the grid (rows/cols) and tile positions (index, row, col,
            label).
        slot_count:
          type: integer
          readOnly: true
        is_active:
          type: boolean
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
        modified:
          type: string
          format: date-time
          readOnly: true
      required:
        - code
        - created
        - description
        - id
        - is_active
        - modified
        - name
        - slot_count
        - slot_layout
    NavbarApp:
      type: object
      description: |-
        Per-app entry in the nine-dots launcher dashboard.

        This is a plain ``serializers.Serializer`` rather than a
        ``ModelSerializer`` because the response joins data from two models
        — ``oauth2_provider.Application`` (client_id, name) and
        ``OAuthAppProfile`` (navbar_*, developer_name, icon fallback) — and
        would need a Meta.model that fits neither naturally.
      properties:
        id:
          type: string
          description: OAuth2 Application.client_id — stable identifier for this app.
        name:
          type: string
          description: '''<developer_name> / <application_name>'' — full descriptive label.'
        label:
          type: string
          description: Short launcher display name (OAuthAppProfile.navbar_label).
        target_url:
          type: string
          format: uri
          description: Destination when the launcher tile is clicked.
        category:
          type: string
          description: Free-text grouping for the dropdown.
        icon_url:
          type: string
          description: >-
            Best-available icon URL — today the OAuthAppProfile.logo_url,
            api#126 will prefer uploaded logo.
        is_visible:
          type: boolean
          default: true
          description: >-
            Present only when called with ?include_hidden=true. True = visible
            in launcher; False = hidden by owner. Absent in the normal launcher
            list (all entries are visible by definition).
      required:
        - category
        - icon_url
        - id
        - label
        - name
        - target_url
    NewPatientDraftDetail:
      type: object
      description: |-
        Full read shape for the detail endpoint.

        Carries every JSON bucket plus lifecycle metadata and the
        consent signature.  ``signature_b64`` is emitted as a
        ``data:image/png;base64,<...>`` data URL so the web app can drop
        it directly into an ``<img src="...">`` tag without a second
        fetch.  The blob is intentionally LIST-omitted (see
        :class:`NewPatientDraftListItemSerializer`) -- inlining it on
        every row would bloat the triage table response.  Typical
        signatures are well under 100 KB so we don't paginate or trim
        on detail.

        The endpoint is already gated to manager-or-above via
        ``IsManagerOrAbove`` (api#761 follow-up) so exposing the PHI
        blob here does not widen the audience.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        practice:
          type: string
          format: uuid
          readOnly: true
          description: Practice the draft was submitted to.
        submitted_via:
          allOf:
            - $ref: '#/components/schemas/SubmittedViaDb2Enum'
          readOnly: true
          description: |-
            Source channel for the submission.

            * `kiosk` - Kiosk
            * `portal` - Patient Portal
            * `staff_form` - Staff-entered form
        submitted_at:
          type: string
          format: date-time
          readOnly: true
          description: When the submission was received by the API.
        tablet:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: >-
            Tablet (kiosk) that submitted this draft; null for portal /
            staff-form drafts.
        personal_info:
          readOnly: true
          description: Patient-supplied demographics; mirrors Patient model field names.
        insurance_info:
          readOnly: true
          description: Insurance demographics; photo refs point at apps.doccenter rows.
        medical_history:
          readOnly: true
          description: Patient-supplied medical history; mirrors clinical models' shape.
        signature_b64:
          type: string
          nullable: true
          description: >-
            Consent capture as a ``data:image/png;base64,<...>`` data URL, or
            ``null`` if the draft has no signature on file.
          readOnly: true
        status:
          allOf:
            - $ref: '#/components/schemas/Status53dEnum'
          readOnly: true
          description: |-
            Lifecycle state.

            * `pending` - Pending review
            * `ready_for_review` - Ready for review
            * `approved` - Approved -- new patient created
            * `converted` - Converted to new patient
            * `merged` - Merged into existing patient
            * `rejected` - Rejected
            * `dismissed` - Dismissed
        reviewed_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff user who took the approve / merge / reject action.
        reviewed_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When the draft was approved / merged / rejected.
        merged_into_patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: 'When status=merged: the existing patient that absorbed this draft.'
        approved_patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: 'When status=approved: the new patient created from this draft.'
        rejection_reason:
          type: string
          readOnly: true
          description: Free-text reason for rejection; empty for non-rejected drafts.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - approved_patient
        - created
        - id
        - insurance_info
        - medical_history
        - merged_into_patient
        - modified
        - personal_info
        - practice
        - rejection_reason
        - reviewed_at
        - reviewed_by
        - signature_b64
        - status
        - submitted_at
        - submitted_via
        - tablet
    NewPatientDraftListItem:
      type: object
      description: |-
        Compact read shape for the list endpoint.

        Carries enough demographics to render a triage table without
        pulling the full JSON buckets (which can be sizeable when a kiosk
        crammed every optional field).  The list view trims to ``pending``
        drafts by default; the front-end can filter on status if it wants
        a fuller picture.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        submitted_via:
          type: string
          readOnly: true
        submitted_at:
          type: string
          format: date-time
          readOnly: true
          description: When the submission was received by the API.
        status:
          type: string
          readOnly: true
        first_name:
          type: string
          readOnly: true
        last_name:
          type: string
          readOnly: true
        date_of_birth:
          type: string
          readOnly: true
        approved_patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: 'When status=approved: the new patient created from this draft.'
        merged_into_patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: 'When status=merged: the existing patient that absorbed this draft.'
      required:
        - approved_patient
        - date_of_birth
        - first_name
        - id
        - last_name
        - merged_into_patient
        - status
        - submitted_at
        - submitted_via
    NewPatientDraftMergeRequest:
      type: object
      description: >-
        Body for the merge action.


        ``target_patient_uid`` is the existing
        :class:`apps.patients.models.Patient`

        that should absorb the draft's insurance + medical history.  The

        view verifies the target belongs to the calling practice -- the

        serializer just validates the shape.
      properties:
        target_patient_uid:
          type: string
          format: uuid
          description: UUID of the existing patient that should absorb this draft.
      required:
        - target_patient_uid
    NewPatientDraftRejectRequest:
      type: object
      description: |-
        Body for the reject action.

        ``reason`` is free text.  Required so a future audit reviewer can
        reconstruct the manager's rationale without joining against any
        other source.
      properties:
        reason:
          type: string
          minLength: 1
          description: Free-text reason for rejection; stays on the row for audit.
          maxLength: 255
      required:
        - reason
    NewPatientDraftSubmitRequest:
      type: object
      description: |-
        Body for the kiosk POST that creates a draft.

        All four data buckets are optional -- the kiosk's wizard might
        submit a partial draft (demographics + signature only) when the
        patient gives up midway and the front desk still wants the row
        so they can call back.  The signature blob is base64 on the wire
        and stored as raw bytes server-side; the kiosk's pad already
        produces a base64-encoded image.
      properties:
        submitted_via:
          allOf:
            - $ref: '#/components/schemas/SubmittedViaDb2Enum'
          default: kiosk
          description: |-
            Source channel; defaults to ``kiosk``.

            * `kiosk` - Kiosk
            * `portal` - Patient Portal
            * `staff_form` - Staff-entered form
        personal_info:
          description: >-
            Demographics; mirrors Patient model field names so approve can copy
            1:1.
        insurance_info:
          description: Insurance demographics; photo refs point at doccenter rows.
        medical_history:
          description: >-
            Conditions / allergies / medications; mirrors clinical models'
            shape.
        signature_b64:
          type: string
          default: ''
          description: >-
            Base64-encoded raw signed-consent capture (image or pad blob). 
            Stored as bytes server-side; legal evidence.
    NoteTemplate:
      type: object
      description: Full CRUD serializer for clinical note templates.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          maxLength: 255
        category:
          oneOf:
            - $ref: '#/components/schemas/CategoryEnum'
            - $ref: '#/components/schemas/BlankEnum'
        content:
          type: string
          description: >-
            Template body. Supports placeholder tokens like {{tooth}},
            {{provider}}.
        is_active:
          type: boolean
        sort_order:
          type: integer
          maximum: 2147483647
          minimum: 0
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - content
        - created
        - id
        - modified
        - name
    NoteTemplateRequest:
      type: object
      description: Full CRUD serializer for clinical note templates.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        category:
          oneOf:
            - $ref: '#/components/schemas/CategoryEnum'
            - $ref: '#/components/schemas/BlankEnum'
        content:
          type: string
          minLength: 1
          description: >-
            Template body. Supports placeholder tokens like {{tooth}},
            {{provider}}.
        is_active:
          type: boolean
        sort_order:
          type: integer
          maximum: 2147483647
          minimum: 0
        custom_data: {}
      required:
        - content
        - name
    NoteTypeEnum:
      enum:
        - clinical
        - exam
        - hygiene
        - consult
        - phone
        - lab
        - other
      type: string
      description: |-
        * `clinical` - Clinical Note
        * `exam` - Exam Note
        * `hygiene` - Hygiene Note
        * `consult` - Consult Note
        * `phone` - Phone Note
        * `lab` - Lab Note
        * `other` - Other
    NullEnum:
      enum:
        - null
    OcrBackfillRequestRequest:
      type: object
      description: |-
        Request payload for POST /api/v1/admin/practice/<uuid>/ocr/backfill/.

        All operational knobs are optional; ``reason`` is the only mandatory
        field.  It's recorded in the audit metadata so every triggered run is
        self-explaining -- we never want a backfill kicked off without the
        "why" sitting next to it (mirrors the pattern from
        :class:`AdminToggleActiveSerializer`).
      properties:
        reason:
          type: string
          minLength: 5
          description: Free-text justification recorded in the audit log (min 5 chars).
          maxLength: 1024
        limit:
          type: integer
          maximum: 100000
          minimum: 1
          nullable: true
          description: Cap the number of OCR tasks dispatched. Defaults to no cap.
        mime:
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 100
          nullable: true
          description: >-
            Override the default mime allow-list. Defaults to
            ['application/pdf', 'image/jpeg', 'image/png', 'image/tiff',
            'image/webp'].
        force:
          type: boolean
          default: false
          description: Re-OCR documents that already have a DocumentOcrResult row.
        dry_run:
          type: boolean
          default: false
          description: Compute the would-be queued count without dispatching tasks.
      required:
        - reason
    OcrBackfillResponse:
      type: object
      description: |-
        Response payload for the OCR-backfill trigger endpoint.

        ``queued`` is the actual dispatch count (or the would-be count when
        ``dry_run=True``); ``total_matched`` is the count of documents that
        matched the filter *before* the ``limit`` cap, so the support UI can
        show "queued 50 of 412 pending".
      properties:
        queued:
          type: integer
          description: Number of OCR tasks dispatched (or would-be on dry-run).
        errored:
          type: integer
          description: Number of documents whose dispatch raised an exception.
        total_matched:
          type: integer
          description: Total documents matching the filter before the limit cap.
        dry_run:
          type: boolean
          description: True when no tasks were dispatched.
      required:
        - dry_run
        - errored
        - queued
        - total_matched
    OcrStatusResponse:
      type: object
      description: |-
        Response payload for GET /api/v1/admin/practice/<uuid>/ocr/status/.

        Lets the support UI render a "X of Y documents OCR'd" progress bar
        without paginating either list.  Counts are scoped to non-deleted
        documents in the supported mime types -- the same universe that the
        backfill operates over, so ``pending`` matches what a default backfill
        would queue.
      properties:
        documents_total:
          type: integer
          description: Total OCR-eligible documents for the practice.
        ocr_results_total:
          type: integer
          description: Documents that already have a DocumentOcrResult row.
        pending:
          type: integer
          description: documents_total - ocr_results_total.
      required:
        - documents_total
        - ocr_results_total
        - pending
    OpenCloseResponse:
      type: object
      properties:
        date:
          type: string
          format: date
        open:
          type: string
          format: date-time
          nullable: true
          description: >-
            ISO-8601 datetime of the first qualifying appointment's start, with
            the practice's TZ offset baked in (handles DST), or ``null`` when no
            qualifying appointments.
        close:
          type: string
          format: date-time
          nullable: true
          description: >-
            ISO-8601 datetime of the last qualifying appointment's end (``start
            + duration``), with the practice's TZ offset baked in (handles DST),
            or ``null`` when no qualifying appointments.
      required:
        - close
        - date
        - open
    Operatory:
      type: object
      description: >-
        GH-711: ``provider_dentist`` / ``provider_hygienist`` must belong to URL
        practice.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          maxLength: 100
        abbreviation:
          type: string
          maxLength: 20
        provider_dentist:
          type: string
          format: uuid
          nullable: true
          description: Default dentist assigned to this chair
        provider_dentist_name:
          type: string
          readOnly: true
        provider_hygienist:
          type: string
          format: uuid
          nullable: true
          description: Default hygienist assigned to this chair
        provider_hygienist_name:
          type: string
          readOnly: true
        is_hygiene:
          type: boolean
        is_hidden:
          type: boolean
        is_bookable:
          type: boolean
          description: >-
            Operatories where is_bookable=False are skipped by the slot-finder.
            Used for PMS 'overflow' or 'notes-only' pseudo-chairs that host
            free-text annotations but aren't real rooms.
        sort_order:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Display ordering in schedule views
        is_web_sched:
          type: boolean
          description: Available for patient online scheduling
        custom_data: {}
      required:
        - id
        - name
        - provider_dentist_name
        - provider_hygienist_name
    OperatoryPresence:
      type: object
      description: |-
        Read serializer for an active chair-presence row.

        Used by the practice-wide list endpoint and the per-chair occupant
        lookup.  Returns flat IDs plus the occupant's display name so the
        messaging menu can render "Jane S. at OP 3" without an extra
        round-trip.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        operatory_id:
          type: string
          format: uuid
          readOnly: true
        user_id:
          type: string
          format: uuid
          readOnly: true
        first_name:
          type: string
          readOnly: true
        last_name:
          type: string
          readOnly: true
        claimed_at:
          type: string
          format: date-time
          readOnly: true
          description: When the user landed on the Clinical view for this chair.
        auto_release_at:
          type: string
          format: date-time
          readOnly: true
          description: >-
            Hard sanity stop: even if the user never explicitly leaves the
            chair, the stale-presence sweep nulls active claims past this
            instant (~one clinical shift past ``claimed_at``).
      required:
        - auto_release_at
        - claimed_at
        - first_name
        - id
        - last_name
        - operatory_id
        - user_id
    OperatoryPresenceWriteRequest:
      type: object
      description: 'Body for claim / release: just the operatory the caller targets.'
      properties:
        operatory_id:
          type: string
          format: uuid
      required:
        - operatory_id
    OperatoryRequest:
      type: object
      description: >-
        GH-711: ``provider_dentist`` / ``provider_hygienist`` must belong to URL
        practice.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        abbreviation:
          type: string
          maxLength: 20
        provider_dentist:
          type: string
          format: uuid
          nullable: true
          description: Default dentist assigned to this chair
        provider_hygienist:
          type: string
          format: uuid
          nullable: true
          description: Default hygienist assigned to this chair
        is_hygiene:
          type: boolean
        is_hidden:
          type: boolean
        is_bookable:
          type: boolean
          description: >-
            Operatories where is_bookable=False are skipped by the slot-finder.
            Used for PMS 'overflow' or 'notes-only' pseudo-chairs that host
            free-text annotations but aren't real rooms.
        sort_order:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Display ordering in schedule views
        is_web_sched:
          type: boolean
          description: Available for patient online scheduling
        custom_data: {}
      required:
        - name
    OperatoryWalkIn:
      type: object
      description: Read-only representation of an :class:`OperatoryWalkIn` row.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        practice:
          type: string
          format: uuid
          readOnly: true
        operatory:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        patient_name:
          type: string
          readOnly: true
        seated_at:
          type: string
          format: date-time
          readOnly: true
          description: When the patient was seated as a walk-in.
        dismissed_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When the walk-in row was closed.  NULL while active.
        seated_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: The staff member who recorded the walk-in (for audit).
      required:
        - dismissed_at
        - id
        - operatory
        - patient
        - patient_name
        - practice
        - seated_at
        - seated_by
    OperatoryWalkInCreateRequest:
      type: object
      description: |-
        Request body for ``POST .../operatory/<id>/walkin/`` (GH-715).

        A walk-in row records that a patient is currently seated in an
        operatory without a pre-existing :class:`Appointment`.  The view
        layer enforces practice-scoping on the patient (the patient must
        belong to the same practice as the operatory) and translates the
        DB-level conditional unique constraint into a friendly 400 if the
        chair is already occupied by another active walk-in.
      properties:
        patient:
          type: string
          format: uuid
      required:
        - patient
    OptedOutReasonEnum:
      enum:
        - patient_request_stop
        - staff_action
        - error_bounce
      type: string
      description: |-
        * `patient_request_stop` - Patient texted STOP / UNSUBSCRIBE
        * `staff_action` - Staff opted patient out
        * `error_bounce` - Auto opt-out after delivery failures
    OutcomeEnum:
      enum:
        - success
        - no_answer
        - voicemail
        - bounced
        - bad_number
        - other
      type: string
      description: |-
        * `success` - Success
        * `no_answer` - No Answer
        * `voicemail` - Voicemail
        * `bounced` - Bounced
        * `bad_number` - Bad Number
        * `other` - Other
    OverviewPracticeRow:
      type: object
      description: Per-practice KPI row inside the overview response.
      properties:
        practice_id:
          type: string
        practice_name:
          type: string
        today_production:
          type: string
        week_production:
          type: string
        ar_total:
          type: string
        no_show_rate_pct:
          type: string
      required:
        - ar_total
        - no_show_rate_pct
        - practice_id
        - practice_name
        - today_production
        - week_production
    OverviewResponse:
      type: object
      description: Top-level overview response.
      properties:
        as_of:
          type: string
        totals:
          $ref: '#/components/schemas/OverviewTotals'
        practices:
          type: array
          items:
            $ref: '#/components/schemas/OverviewPracticeRow'
      required:
        - as_of
        - practices
        - totals
    OverviewTotals:
      type: object
      description: Cross-practice aggregate tiles for the overview landing page.
      properties:
        today_production:
          type: string
        week_production:
          type: string
        ar_total:
          type: string
        no_show_rate_pct:
          type: string
        practice_count:
          type: integer
      required:
        - ar_total
        - no_show_rate_pct
        - practice_count
        - today_production
        - week_production
    PaginatedAdjustmentList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/Adjustment'
    PaginatedAdminOAuthAppList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/AdminOAuthApp'
    PaginatedAnesthesiaRecordList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/AnesthesiaRecord'
    PaginatedApiStatsRowList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/ApiStatsRow'
    PaginatedAppointmentList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/Appointment'
    PaginatedAutoNoteList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/AutoNote'
    PaginatedBannerList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/Banner'
    PaginatedClaimStatusLogList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/ClaimStatusLog'
    PaginatedClinicalNoteList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/ClinicalNote'
    PaginatedCommLogList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/CommLog'
    PaginatedDocumentCategoryList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/DocumentCategory'
    PaginatedDocumentList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/Document'
    PaginatedExamImageList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/ExamImage'
    PaginatedExamList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/Exam'
    PaginatedExamTemplateList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/ExamTemplate'
    PaginatedExamTemplateSlotList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/ExamTemplateSlot'
    PaginatedFaxList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/Fax'
    PaginatedFeeScheduleEntryList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/FeeScheduleEntry'
    PaginatedFeeScheduleListList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/FeeScheduleList'
    PaginatedFeedbackListList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/FeedbackList'
    PaginatedFormBundleList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/FormBundle'
    PaginatedFormFieldMappingProposalList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/FormFieldMappingProposal'
    PaginatedFormSendOutListList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/FormSendOutList'
    PaginatedFormSubmissionList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/FormSubmission'
    PaginatedFormTemplateList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/FormTemplate'
    PaginatedImageMountList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/ImageMount'
    PaginatedImportIssueListList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/ImportIssueList'
    PaginatedInboxItemList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/InboxItem'
    PaginatedInsuranceBenefitList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/InsuranceBenefit'
    PaginatedInsuranceCarrierList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/InsuranceCarrier'
    PaginatedInsuranceClaimList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/InsuranceClaim'
    PaginatedInsurancePlanListList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/InsurancePlanList'
    PaginatedInsuranceVerificationList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/InsuranceVerification'
    PaginatedInviteCodeList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/InviteCode'
    PaginatedKioskSessionDetailList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/KioskSessionDetail'
    PaginatedMountTemplateList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/MountTemplate'
    PaginatedNewPatientDraftListItemList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/NewPatientDraftListItem'
    PaginatedNoteTemplateList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/NoteTemplate'
    PaginatedOperatoryList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/Operatory'
    PaginatedPatientAllergyList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/PatientAllergy'
    PaginatedPatientConditionList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/PatientCondition'
    PaginatedPatientEducationList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/PatientEducation'
    PaginatedPatientFeedbackListList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/PatientFeedbackList'
    PaginatedPatientInboxListItemList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/PatientInboxListItem'
    PaginatedPatientInsuranceList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/PatientInsurance'
    PaginatedPatientList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/Patient'
    PaginatedPatientMedicationList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/PatientMedication'
    PaginatedPatientSubmissionListList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/PatientSubmissionList'
    PaginatedPaymentIntentList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/PaymentIntent'
    PaginatedPaymentPlanList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/PaymentPlan'
    PaginatedPaymentRecordList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/PaymentRecord'
    PaginatedPerioExamList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/PerioExam'
    PaginatedPortalAppointmentList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/PortalAppointment'
    PaginatedPortalMessageList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/PortalMessage'
    PaginatedPracticeList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/Practice'
    PaginatedPrescriptionList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/Prescription'
    PaginatedPrescriptionTemplateList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/PrescriptionTemplate'
    PaginatedProcedureCodeList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/ProcedureCode'
    PaginatedProcedureList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/Procedure'
    PaginatedProviderList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/Provider'
    PaginatedRecallList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/Recall'
    PaginatedReferralSourceList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/ReferralSource'
    PaginatedSMSMessageList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/SMSMessage'
    PaginatedScheduleOverrideList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/ScheduleOverride'
    PaginatedScheduleTemplateList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/ScheduleTemplate'
    PaginatedShareListItemList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/ShareListItem'
    PaginatedSignatureListList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/SignatureList'
    PaginatedSourceList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/Source'
    PaginatedSupportPatientList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/SupportPatient'
    PaginatedSupportSessionListList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/SupportSessionList'
    PaginatedToothConditionList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/ToothCondition'
    PaginatedTreatmentPlanItemList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/TreatmentPlanItem'
    PaginatedTreatmentPlanList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/TreatmentPlan'
    PaginatedUserList:
      type: object
      required:
        - results
      properties:
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?cursor=cD00ODY%3D"
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?cursor=cj0xJnA9NDg3
        results:
          type: array
          items:
            $ref: '#/components/schemas/User'
    PaginatedVitalSignList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/VitalSign'
    PaginatedVoicemailInboxRowList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/VoicemailInboxRow'
    PaginatedWebhookDeliveryList:
      type: object
      required:
        - count
        - results
      properties:
        count:
          type: integer
          example: 123
        next:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=4
        previous:
          type: string
          nullable: true
          format: uri
          example: http://api.example.org/accounts/?page=2
        results:
          type: array
          items:
            $ref: '#/components/schemas/WebhookDelivery'
    PairCompletionRequest:
      type: object
      description: |-
        Validate ``POST /v1/oauth/tablet-pair/`` payloads.

        The pair_token is the JWT staff handed the tablet via QR; the
        public_key is base64-encoded DER bytes (SubjectPublicKeyInfo) for
        the ECDSA P-256 keypair the tablet generated locally.

        We deliberately do NOT verify the public_key bytes are a
        well-formed EC point at the serializer layer: that requires
        pulling in a heavyweight crypto dependency (`cryptography`), and
        the column is currently only used as forensic evidence -- the
        runtime auth is the bound OAuth tokens, not a signed request
        envelope.  If/when we layer signed-request enforcement on top, the
        validation moves here.
      properties:
        pair_token:
          type: string
          minLength: 1
          description: Pair-token JWT issued by the staff-side pair-token endpoint.
          maxLength: 4096
        public_key:
          type: string
          minLength: 1
          description: >-
            Base64-encoded ECDSA P-256 public key (DER SubjectPublicKeyInfo) the
            tablet just generated.
          maxLength: 4096
      required:
        - pair_token
        - public_key
    PairTokenCreateRequest:
      type: object
      description: |-
        Validate ``POST .../tablets/pair-token/`` payloads.

        Only one field today (the staff-chosen tablet name) but kept as a
        serializer so future fields (e.g. per-pair scope override) land
        without breaking the surface.  The collision pre-flight against
        already-paired-and-active tablets lives in the view -- the
        serializer only enforces shape.
      properties:
        name:
          type: string
          minLength: 1
          description: Staff-assigned label for the tablet (e.g. 'Front Desk 1').
          maxLength: 64
      required:
        - name
    PairTokenResponse:
      type: object
      description: |-
        Response shape returned by ``POST .../tablets/pair-token/`` on 201.

        Exists solely so drf-spectacular emits the response body in the
        OpenAPI schema -- the web side's :mod:`openapi-typescript` codegen
        needs an explicit serializer to derive types from (api#685).  The
        fields mirror the inline-dict the view constructs; if either
        changes, change the other.
      properties:
        pair_token:
          type: string
          description: >-
            Raw JWT (only useful to a tablet capable of decoding); included for
            non-QR flows like a future print-and-hand-off workflow.
        qr_payload:
          type: string
          format: uri
          description: Absolute URL the tablet scans. Web renders this as a QR code.
        expires_at:
          type: string
          format: date-time
          description: >-
            ISO-8601 UTC timestamp of the pair token's expiry (5 minutes after
            mint).
      required:
        - expires_at
        - pair_token
        - qr_payload
    ParticipantSummary:
      type: object
      description: Compact participant representation for conversation lists.
      properties:
        user_id:
          type: string
          format: uuid
          readOnly: true
        first_name:
          type: string
          readOnly: true
        last_name:
          type: string
          readOnly: true
        last_read_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - first_name
        - last_name
        - last_read_at
        - user_id
    PatchedAdminOAuthAppThrottleModeRequest:
      type: object
      description: |-
        Request payload for ``PATCH /api/v1/admin/oauth-app/<client_id>/``.

        Mirrors :class:`AdminToggleActiveSerializer` — a free-text ``reason``
        is required and recorded in the audit log so every throttle-mode
        flip is self-explaining.  The five-character minimum matches the
        rest of the admin API's "no empty justifications" rule.
      properties:
        throttle_mode:
          allOf:
            - $ref: '#/components/schemas/ThrottleModeEnum'
          description: >-
            Target throttle mode. ``default`` clears any per-app override and
            inherits the global setting.


            * `default` - Default (inherit global)

            * `audit` - Audit

            * `enforce` - Enforce
        reason:
          type: string
          minLength: 5
          description: Free-text justification recorded in the audit log (min 5 chars).
          maxLength: 1024
    PatchedAdminToggleActiveRequest:
      type: object
      description: |-
        Request payload for toggling ``is_active`` on a resource.

        A free-text ``reason`` is required. It is recorded in the audit log so
        every enable/disable decision is self-explaining — we never want a
        raw ``is_active`` flip without the "why" sitting next to it.
      properties:
        is_active:
          type: boolean
          description: Set to true to enable, false to disable.
        reason:
          type: string
          minLength: 5
          description: Free-text justification recorded in the audit log (min 5 chars).
          maxLength: 1024
    PatchedAnesthesiaRecordWriteRequest:
      type: object
      description: |-
        Write serializer for creating and updating anesthesia records.

        The ``patient`` field is excluded because it is set from the URL path
        by the view's ``perform_create`` method.
      properties:
        appointment:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        agent:
          $ref: '#/components/schemas/AgentEnum'
        agent_other:
          type: string
          description: If agent=other, specify here
          maxLength: 100
        route:
          $ref: '#/components/schemas/RouteEnum'
        site:
          type: string
          description: 'e.g. #14 buccal infiltration, right IAN block'
          maxLength: 100
        carpules:
          type: string
          format: decimal
          pattern: ^-?\d{0,2}(?:\.\d{0,1})?$
          description: Number of carpules administered
        lot_number:
          type: string
          maxLength: 50
        expiration_date:
          type: string
          format: date
          nullable: true
        administered_at:
          type: string
          format: date-time
          description: When the anesthesia was administered
        note:
          type: string
          description: Complications, reactions, or additional notes
        custom_data: {}
    PatchedAppVisibilityToggleRequest:
      type: object
      description: PATCH body for toggling an app's visibility in a practice's navbar.
      properties:
        is_visible:
          type: boolean
    PatchedAppointmentRequest:
      type: object
      description: |-
        Read/write serializer for appointments.

        GH-711: the ``patient`` / ``provider`` / ``provider_hygienist`` /
        ``operatory`` / ``treatment_plan`` FKs are validated against the URL
        practice on POST/PATCH.  Inherited by the bulk variant
        (:class:`AppointmentBulkWriteSerializer`) so the first-party bulk path
        can't drop an appointment under practice A's URL with practice B's
        patient/provider/operatory.
      properties:
        patient:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        provider_hygienist:
          type: string
          format: uuid
          nullable: true
        operatory:
          type: string
          format: uuid
          nullable: true
        treatment_plan:
          type: string
          format: uuid
          nullable: true
          description: Treatment plan this appointment was scheduled from
        date_time:
          type: string
          format: date-time
          nullable: true
        duration_minutes:
          type: integer
          maximum: 2147483647
          minimum: 0
        confirmed_status:
          oneOf:
            - $ref: '#/components/schemas/ConfirmedStatusEnum'
            - $ref: '#/components/schemas/BlankEnum'
        appointment_type:
          type: string
          description: e.g. New Patient Exam, Crown Prep
          maxLength: 100
        is_hygiene:
          type: boolean
        is_new_patient:
          type: boolean
        procedure_description:
          type: string
        note:
          type: string
        cancellation_reason:
          type: string
          description: Patient-supplied reason for cancellation
        reschedule_requested:
          type: boolean
          description: Patient asked the front desk to reschedule
        arrived_at:
          type: string
          format: date-time
          nullable: true
        seated_at:
          type: string
          format: date-time
          nullable: true
        ready_for_doctor_at:
          type: string
          format: date-time
          nullable: true
          description: When hygienist signaled ready for doctor exam
        ready_for_walkout_at:
          type: string
          format: date-time
          nullable: true
          description: When patient is ready for walkout (checkout/scheduling)
        dismissed_at:
          type: string
          format: date-time
          nullable: true
        dollars_scheduled:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Scheduled procedure total from the source PMS; nullable when unknown
        custom_data: {}
    PatchedAppointmentUpdateRequest:
      type: object
      description: >-
        Staff PUT/PATCH serializer — pins ``status`` read-only.


        Staff UIs must route status changes through the ``/transition/``
        endpoint,

        which enforces :data:`apps.scheduling.services.ALLOWED_TRANSITIONS` and

        writes an :class:`AppointmentStatusLog` row.  This serializer removes

        ``status`` from the writable field set so a stray ``PATCH {"status":
        ...}``

        from the web app is silently ignored rather than bypassing the state

        machine.


        The bulk-import path intentionally uses

        :class:`AppointmentBulkWriteSerializer` instead — the source PMS is

        authoritative for status and must be able to write it directly.
      properties:
        patient:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        provider_hygienist:
          type: string
          format: uuid
          nullable: true
        operatory:
          type: string
          format: uuid
          nullable: true
        treatment_plan:
          type: string
          format: uuid
          nullable: true
          description: Treatment plan this appointment was scheduled from
        date_time:
          type: string
          format: date-time
          nullable: true
        duration_minutes:
          type: integer
          maximum: 2147483647
          minimum: 0
        confirmed_status:
          oneOf:
            - $ref: '#/components/schemas/ConfirmedStatusEnum'
            - $ref: '#/components/schemas/BlankEnum'
        appointment_type:
          type: string
          description: e.g. New Patient Exam, Crown Prep
          maxLength: 100
        is_hygiene:
          type: boolean
        is_new_patient:
          type: boolean
        procedure_description:
          type: string
        note:
          type: string
        cancellation_reason:
          type: string
          description: Patient-supplied reason for cancellation
        reschedule_requested:
          type: boolean
          description: Patient asked the front desk to reschedule
        arrived_at:
          type: string
          format: date-time
          nullable: true
        seated_at:
          type: string
          format: date-time
          nullable: true
        ready_for_doctor_at:
          type: string
          format: date-time
          nullable: true
          description: When hygienist signaled ready for doctor exam
        ready_for_walkout_at:
          type: string
          format: date-time
          nullable: true
          description: When patient is ready for walkout (checkout/scheduling)
        dismissed_at:
          type: string
          format: date-time
          nullable: true
        dollars_scheduled:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Scheduled procedure total from the source PMS; nullable when unknown
        scheduled_procedures:
          type: array
          items:
            $ref: '#/components/schemas/AppointmentProcedureRequest'
        custom_data: {}
        allow_overbooking:
          type: boolean
          default: false
        skip_validation:
          type: boolean
          default: false
    PatchedAutoNoteRequest:
      type: object
      description: |-
        Read/write serializer for :class:`AutoNote` chairside macros.

        The ``prompts`` field is a JSON-typed list of operator-fill widget
        specs validated by :func:`_validate_autonote_prompts`.  See the model
        docstring for the canonical contract.
      properties:
        name:
          type: string
          minLength: 1
          description: Operator-visible macro label, e.g. 'Perio Max'.
          maxLength: 128
        body:
          type: string
          minLength: 1
          description: >-
            Template body with single-braced {key} placeholders matched against
            prompts[].key.
        category:
          type: string
          description: >-
            Free-form grouping (e.g. 'Hygiene', 'Endo').  PMS-supplied; not
            enumerated.
          maxLength: 64
        prompts:
          type: array
          items:
            type: object
            additionalProperties: {}
          description: Operator-fill widget specs.  See AutoNote model docs for shape.
        custom_data: {}
    PatchedBannerRequest:
      type: object
      description: |-
        Full CRUD payload for a Banner.

        Exactly one of ``client`` / ``practice`` must be supplied.  This is
        enforced both by the model-level CheckConstraint and by ``validate()``
        here so the API returns a 400 with a useful message instead of an
        IntegrityError.

        The ``starts_at`` / ``expires_at`` fields must be timezone-aware UTC
        datetimes per the project-wide convention; DRF's default DateTimeField
        handles parsing/output in ISO-8601 with timezone offsets.
      properties:
        title:
          type: string
          description: Optional short heading shown above the message.
          maxLength: 120
        message:
          type: string
          minLength: 1
          description: Plain-text banner body.  HTML is not rendered by the client.
        severity:
          allOf:
            - $ref: '#/components/schemas/SeverityEnum'
          description: |-
            Visual treatment hint for the frontend (info/warning/urgent).

            * `info` - Info
            * `warning` - Warning
            * `urgent` - Urgent
        starts_at:
          type: string
          format: date-time
          description: UTC datetime the banner becomes active.  Defaults to now.
        expires_at:
          type: string
          format: date-time
          description: UTC datetime after which the banner is no longer active.
        client:
          type: string
          format: uuid
          nullable: true
          description: Client this banner targets.  Mutually exclusive with `practice`.
        practice:
          type: string
          format: uuid
          nullable: true
          description: Practice this banner targets.  Mutually exclusive with `client`.
    PatchedClinicalNoteWriteRequest:
      type: object
      description: |-
        Write serializer for creating and updating clinical notes.

        Enforces HIPAA immutability: signed notes cannot be modified.
        The ``patient`` field is excluded because it is set from the URL path
        by the view's ``perform_create`` method.

        GH-711: when ``make_bulk_serializer`` injects ``patient``, the FK plus
        ``appointment`` / ``provider`` / ``template`` / ``parent_note`` are all
        validated against the URL practice.
      properties:
        appointment:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        template:
          type: string
          format: uuid
          nullable: true
          description: >-
            Template used to create this note (reference only, content is
            copied)
        note_type:
          $ref: '#/components/schemas/NoteTypeEnum'
        subject:
          type: string
          maxLength: 200
        content:
          type: string
          description: Plain text or structured note content
        note_date:
          type: string
          format: date
          nullable: true
        is_addendum:
          type: boolean
          description: Addendum to a previous note
        parent_note:
          type: string
          format: uuid
          nullable: true
          description: If is_addendum=True, the original note this amends
        custom_data: {}
    PatchedCommLogBulkWriteRequest:
      type: object
      description: >-
        Write serializer for practice-wide bulk commlog import.


        Includes ``patient`` (required) since the endpoint is not
        patient-scoped.

        Used by :class:`PracticeCommLogListCreateView` for POST/PATCH arrays
        from

        sync tools that iterate practice-wide instead of per-patient.


        Accepts ``is_imported`` so escape-opendental / escape-eaglesoft can

        flag every row they ingest as imported (GH-575).  The model default

        is ``False`` for safety -- if the sync tool forgets to pass the flag,

        the row falls through as a native entry, which is the wrong failure

        direction but at least preserves the historical behaviour while the

        sync tools catch up.
      properties:
        patient:
          type: string
          format: uuid
          nullable: true
        date_time:
          type: string
          format: date-time
          nullable: true
        mode:
          $ref: '#/components/schemas/ModeEnum'
        direction:
          $ref: '#/components/schemas/Direction80cEnum'
        outcome:
          allOf:
            - $ref: '#/components/schemas/OutcomeEnum'
          description: >-
            What happened on the contact attempt.  Only ``success`` rows count
            toward the recall-list ``last_reached_at`` annotation.


            * `success` - Success

            * `no_answer` - No Answer

            * `voicemail` - Voicemail

            * `bounced` - Bounced

            * `bad_number` - Bad Number

            * `other` - Other
        subject:
          type: string
          description: Short label, e.g. 'Recall reminder', 'Treatment plan follow-up'.
          maxLength: 255
        note:
          type: string
        staff_user:
          type: string
          format: uuid
          nullable: true
          description: >-
            Staff member who logged the contact.  Null for system-generated
            entries.
        recall:
          type: string
          format: uuid
          nullable: true
          description: Optional link to the specific recall this contact addresses.
        is_imported:
          type: boolean
          description: >-
            True if this row was imported from a legacy PMS (escape-opendental,
            escape-eaglesoft, ...) rather than created natively in Escape. 
            Imported rows may carry PHI in the ``note`` body that pre-dates the
            Escape BAA / no-PHI-in-comms policy (GH-575); clients must NOT
            expose 'resend' or 'use as template' affordances on imported rows,
            and any future send endpoint that takes a
            ``replicate_of_commlog_id`` MUST refuse imported sources.
        custom_data: {}
    PatchedCommLogRequest:
      type: object
      description: |-
        Read serializer for communication log entries.

        Returns all fields including patient FK and timestamps.
        ``custom_data`` is scoped per OAuth application.

        Surfaces two PHI-aware fields added in GH-575:

        * ``is_imported`` -- read-only flag.  ``True`` means the row was
          brought across by a sync tool (escape-opendental, escape-eaglesoft,
          ...) and pre-dates Escape's "no PHI in email/SMS" policy.  Clients
          MUST suppress any "resend" / "use as template" affordance on these
          rows.  Native rows (``is_imported=False``) are safe to template.
        * ``phi_redacted_note`` -- computed.  The ``note`` body with the PHI
          keyword list (diagnosis, prescription, tooth, crown, ...) replaced
          by ``[REDACTED]``.  Surfaces that have no business showing PHI
          (search snippets, dashboard widgets, notification toasts) consume
          this instead of ``note``.  The raw ``note`` is still returned for
          portal display where PHI is appropriate.
      properties:
        patient:
          type: string
          format: uuid
          nullable: true
        date_time:
          type: string
          format: date-time
          nullable: true
        mode:
          $ref: '#/components/schemas/ModeEnum'
        direction:
          $ref: '#/components/schemas/Direction80cEnum'
        outcome:
          allOf:
            - $ref: '#/components/schemas/OutcomeEnum'
          description: >-
            What happened on the contact attempt.  Only ``success`` rows count
            toward the recall-list ``last_reached_at`` annotation.


            * `success` - Success

            * `no_answer` - No Answer

            * `voicemail` - Voicemail

            * `bounced` - Bounced

            * `bad_number` - Bad Number

            * `other` - Other
        subject:
          type: string
          description: Short label, e.g. 'Recall reminder', 'Treatment plan follow-up'.
          maxLength: 255
        note:
          type: string
        staff_user:
          type: string
          format: uuid
          nullable: true
          description: >-
            Staff member who logged the contact.  Null for system-generated
            entries.
        recall:
          type: string
          format: uuid
          nullable: true
          description: Optional link to the specific recall this contact addresses.
        custom_data: {}
    PatchedCommLogWriteRequest:
      type: object
      description: |-
        Write serializer for creating and updating communication log entries.

        Practice and patient are set from the URL path by the view.

        ``is_imported`` is deliberately NOT writable here -- the patient-
        scoped create endpoint is for native Escape entries, which are
        always ``is_imported=False`` (the model default).  The bulk
        import path uses :class:`CommLogBulkWriteSerializer`, which DOES
        accept ``is_imported`` from sync tools.

        GH-711: ``patient`` (injected on the bulk path) and ``recall`` are
        practice-validated.  ``staff_user`` points at the global User table
        (not practice-scoped) so it is intentionally excluded — the existing
        membership check on user-action endpoints handles its access control.
      properties:
        date_time:
          type: string
          format: date-time
          nullable: true
        mode:
          $ref: '#/components/schemas/ModeEnum'
        direction:
          $ref: '#/components/schemas/Direction80cEnum'
        outcome:
          allOf:
            - $ref: '#/components/schemas/OutcomeEnum'
          description: >-
            What happened on the contact attempt.  Only ``success`` rows count
            toward the recall-list ``last_reached_at`` annotation.


            * `success` - Success

            * `no_answer` - No Answer

            * `voicemail` - Voicemail

            * `bounced` - Bounced

            * `bad_number` - Bad Number

            * `other` - Other
        subject:
          type: string
          description: Short label, e.g. 'Recall reminder', 'Treatment plan follow-up'.
          maxLength: 255
        note:
          type: string
        staff_user:
          type: string
          format: uuid
          nullable: true
          description: >-
            Staff member who logged the contact.  Null for system-generated
            entries.
        recall:
          type: string
          format: uuid
          nullable: true
          description: Optional link to the specific recall this contact addresses.
        custom_data: {}
    PatchedDocumentCategoryRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        slug:
          type: string
          minLength: 1
          maxLength: 50
          pattern: ^[-a-zA-Z0-9_]+$
        label:
          type: string
          minLength: 1
          maxLength: 100
        icon:
          type: string
          description: Lucide icon name
          maxLength: 50
        sort_order:
          type: integer
          maximum: 32767
          minimum: 0
        custom_data: {}
    PatchedDocumentUpdateRequest:
      type: object
      description: >-
        Write serializer for updating document metadata — allows re-linking
        patient.
      properties:
        patient:
          type: string
          format: uuid
          nullable: true
        category:
          type: string
          format: uuid
          nullable: true
        title:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type: string
        caller_id:
          type: string
          description: >-
            E.164 caller-ID for voicemail recordings (e.g. +14155551234).  Used
            by the patient-link suggestion engine downstream of the
            voicemail-to-text pipeline.
          pattern: ^\+\d{1,15}$
          maxLength: 32
        custom_data: {}
    PatchedExamImageRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        exam:
          type: string
          format: uuid
        source:
          type: string
          format: uuid
          nullable: true
        template_slot:
          type: string
          format: uuid
          nullable: true
          description: Which slot in the exam template grid this image fills
        original_filename:
          type: string
          maxLength: 255
        mime_type:
          type: string
          minLength: 1
          maxLength: 50
        image_type:
          oneOf:
            - $ref: '#/components/schemas/ImageTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        capture_date:
          type: string
          format: date
        capture_device:
          type: string
          description: Device name from browser getUserMedia
          maxLength: 255
        tooth_numbers:
          description: 'Universal numbering: 1-32 permanent, A-T primary'
        description:
          type: string
          description: Short description/label for the image
          maxLength: 255
        degrees_rotated:
          type: integer
          maximum: 2147483647
          minimum: -2147483648
          description: 'Rotation state: 0, 90, 180, or 270 degrees'
        is_flipped:
          type: boolean
          description: Horizontal flip state
        display_settings:
          description: 'Viewer display settings: brightness, contrast, inverted, etc.'
        annotations:
          description: Array of annotation objects from the viewer
        notes:
          type: string
        custom_data: {}
    PatchedExamRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        template:
          type: string
          format: uuid
          nullable: true
        name:
          type: string
          description: Optional exam name/label
          maxLength: 255
        exam_date:
          type: string
          format: date
        provider:
          type: string
          format: uuid
          nullable: true
        notes:
          type: string
        is_complete:
          type: boolean
        custom_data: {}
    PatchedExamTemplateRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type: string
        grid_rows:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Number of rows in the exam grid layout
        grid_cols:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Number of columns in the exam grid layout
        source:
          type: string
          format: uuid
          nullable: true
          description: Capture source for this template
        is_active:
          type: boolean
        custom_data: {}
    PatchedFeeScheduleRequest:
      type: object
      description: 'GH-711: ``carrier`` FK must belong to URL practice.'
      properties:
        carrier:
          type: string
          format: uuid
          nullable: true
        name:
          type: string
          minLength: 1
          maxLength: 255
        effective_date:
          type: string
          format: date
          nullable: true
        expiration_date:
          type: string
          format: date
          nullable: true
        notes:
          type: string
        custom_data: {}
    PatchedFormBundleRequest:
      type: object
      description: |-
        Practice-scoped bundle with ordered template membership.

        Write payload accepts ``template_ids`` as an ordered list; reads expose
        ``items`` with the joined template name for display.

        ``is_default_new_patient`` is writable (api#769) — setting it true
        clears the flag on any sibling bundle in the same practice as a
        side-effect of :meth:`FormBundle.save`.  ``template_ids`` is required
        on create but optional on PATCH so a flag-only edit doesn't have to
        re-submit the entire ordered membership.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
        is_default_new_patient:
          type: boolean
          description: >-
            When true, this bundle is the practice's default new-patient intake
            packet (api#769).  Setting this true on a row clears the flag on any
            sibling bundle in the same practice — at most one default per
            practice.  False on any number of rows is fine.
        template_ids:
          type: array
          items:
            type: string
            format: uuid
          writeOnly: true
          description: Ordered list of FormTemplate UUIDs to include.
    PatchedFormTemplateRequest:
      type: object
      description: |-
        Practice-scoped form template with nested fields.

        A ``PATCH`` against a template that has any submissions tied to it bumps
        the version (archives the current row, returns the new one).  Edits on
        a template with no submissions update in place — there's no audit value
        in versioning a draft that no patient has touched.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
        fields:
          type: array
          items:
            $ref: '#/components/schemas/FormFieldRequest'
        prefill_map:
          description: >-
            Map of FormField.label (or template field name) -> dotted attribute
            path on the Patient row that should pre-populate that field on the
            kiosk / portal renderer (api#772).  Shape: ``{field_name:
            'patient.attribute_dotted_path'}``. Example: ``{'First name':
            'first_name', 'Insurance member ID':
            'insurance_enrollments.first.subscriber_id'}``.  Resolution is
            best-effort and read-only -- absent attributes / null values are
            omitted from the response so the renderer treats them as 'user must
            fill in'.  See :func:`apps.forms.prefill.resolve_prefill` for
            traversal rules.
    PatchedImageMountRequest:
      type: object
      description: |-
        List/create serializer for mounts — slots are read-only here.

        Creating a mount with ``template`` will eagerly create one empty
        ``ImageMountSlot`` per tile in the template layout; this happens in
        the view's ``perform_create`` so the serializer stays deterministic.
      properties:
        patient:
          type: string
          format: uuid
        exam:
          type: string
          format: uuid
          nullable: true
          description: Optional exam the mount was initialised from.
        template:
          type: string
          format: uuid
        name:
          type: string
          description: Optional user-supplied label.
          maxLength: 255
        notes:
          type: string
        custom_data: {}
    PatchedImageMountSlotRequest:
      type: object
      description: |-
        Serializer for a single slot on a mount.

        Used both when the slot is read (nested on the mount detail response)
        and when a slot is updated via PATCH. Image assignment validates the
        image belongs to the same patient as the mount.
      properties:
        image:
          type: string
          format: uuid
          nullable: true
        rotation:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: 'Display rotation in degrees: 0, 90, 180 or 270.'
        flipped:
          type: boolean
          description: Horizontal flip.
        label:
          type: string
          maxLength: 255
        custom_data: {}
    PatchedImportIssueDetailRequest:
      type: object
      description: |-
        Detail / PATCH shape for the inbox.

        Writable fields on PATCH: ``status``, ``assigned_to``,
        ``resolution_note``.  Everything else is read-only -- the report
        body is immutable after intake.  Read-only ``assigned_to_email``
        is denormalised so the inbox UI doesn't have to round-trip through
        ``/api/v1/admin/users/`` for every row it renders.
      properties:
        status:
          allOf:
            - $ref: '#/components/schemas/ImportIssueStatusEnum'
          description: |-
            Triage state.

            * `new` - New
            * `triaged` - Triaged
            * `resolved` - Resolved
            * `wontfix` - Won't fix
        assigned_to:
          type: string
          format: uuid
          nullable: true
          description: Staff user who owns this issue's triage, when assigned.
        resolution_note:
          type: string
          description: Free-form note set when the issue moves to resolved/wontfix.
    PatchedInboxItemRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        title:
          type: string
          maxLength: 255
        notes:
          type: string
          description: Staff notes during triage
        custom_data: {}
    PatchedInsuranceBenefitWriteRequest:
      type: object
      description: |-
        Write serializer for insurance benefits.

        The ``procedure_code`` FK is accepted as a write-only PK; responses
        should use :class:`InsuranceBenefitSerializer` to render the nested
        object.  The legacy ``proc_code`` string remains accepted as-is for
        PMS-importer compatibility.
      properties:
        insurance_plan:
          type: string
          format: uuid
          nullable: true
        benefit_type:
          oneOf:
            - $ref: '#/components/schemas/BenefitTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        coverage_category:
          oneOf:
            - $ref: '#/components/schemas/CoverageCategoryEnum'
            - $ref: '#/components/schemas/BlankEnum'
        percent:
          type: string
          format: decimal
          pattern: ^-?\d{0,3}(?:\.\d{0,2})?$
        monetary_amt:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        time_period:
          oneOf:
            - $ref: '#/components/schemas/TimePeriodEnum'
            - $ref: '#/components/schemas/BlankEnum'
        proc_code:
          type: string
          description: Benefit tied to a specific procedure code
          maxLength: 20
        procedure_code:
          type: string
          format: uuid
          nullable: true
        min_age:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Minimum patient age (inclusive) for this benefit to apply
        max_age:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Maximum patient age (inclusive) for this benefit to apply
        waiting_period_months:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Months from effective date before this benefit is available
        custom_data: {}
    PatchedInsuranceCarrierRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        phone:
          type: string
          maxLength: 20
        website:
          type: string
          format: uri
          maxLength: 200
        address_line1:
          type: string
          maxLength: 255
        address_line2:
          type: string
          maxLength: 255
        city:
          type: string
          maxLength: 100
        state:
          type: string
          maxLength: 2
        zip_code:
          type: string
          maxLength: 10
        payer_id:
          type: string
          description: EDI payer ID for clearinghouse submission
          maxLength: 50
        claim_office_id:
          type: string
          description: Separate claim submission office ID
          maxLength: 50
        hpid:
          type: string
          description: Health Plan Identifier
          maxLength: 50
        payment_source:
          type: string
          description: check, eft, virtual_card
          maxLength: 50
        notes:
          type: string
        is_active:
          type: boolean
        custom_data: {}
    PatchedInsuranceClaimWriteRequest:
      type: object
      description: >-
        Serializer for creating and updating insurance claims.


        Supports nested creation of claim procedures via the ``procedures``
        field.

        The ``practice`` and ``status`` fields are set by the view, not the
        client.


        GH-711: when the bulk-array path injects ``patient`` via

        :func:`apps.sync.views_bulk.make_bulk_serializer`, that injected FK plus

        the ``provider`` and ``patient_insurance`` FKs are checked against the

        URL practice.  The staff path's ``patient`` is read-only and set by

        the view from the URL — the validator no-ops in that case since the

        attribute is absent from ``validated_data``.
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        patient_insurance:
          type: string
          format: uuid
          nullable: true
        date_service:
          type: string
          format: date
          nullable: true
        date_sent:
          type: string
          format: date
          nullable: true
        claim_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        ins_pay_est:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        ins_pay_amt:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        ded_applied:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        write_off:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        procedures:
          type: array
          items:
            $ref: '#/components/schemas/ClaimProcedureWriteRequest'
        custom_data: {}
    PatchedInsurancePlanRequest:
      type: object
      description: >-
        Read/write serializer for insurance plans.


        Supports nested ``benefits`` on create and update.  On POST/PATCH the

        serializer accepts a list of benefit payloads (same shape as

        :class:`InsuranceBenefitWriteSerializer`) and materializes them in a

        single ``bulk_create`` keyed to the parent plan's practice.


        GH-711: ``carrier`` and ``fee_schedule`` FKs must belong to URL
        practice.


        Update semantics mirror :class:`TreatmentPlanWriteSerializer`:


        - If ``benefits`` is present in the payload, it fully replaces the
          plan's existing benefits (snapshot-replace).  Once a practice has
          migrated off a legacy PMS the Escape API is the source of truth, so
          sync tools and UI clients that want to update the benefit list send
          the complete new list.
        - If ``benefits`` is omitted, the plan's existing benefits are left
          untouched — callers patching plan-level fields don't need to ship
          the full benefits collection.

        Output shape is unchanged: benefits are always rendered with

        :class:`InsuranceBenefitSerializer` so the nested ``procedure_code``

        object is included.
      properties:
        carrier:
          type: string
          format: uuid
          nullable: true
        plan_name:
          type: string
          maxLength: 255
        group_name:
          type: string
          maxLength: 255
        group_number:
          type: string
          maxLength: 100
        employer_name:
          type: string
          maxLength: 255
        plan_type:
          oneOf:
            - $ref: '#/components/schemas/PlanTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        annual_maximum:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
        individual_deductible:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
        family_deductible:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
        plan_year_begin_month:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Month (1-12) when the plan year resets
        fee_schedule:
          type: string
          format: uuid
          nullable: true
          description: Carrier-specific fee schedule (allowed amounts) for this plan
        notes:
          type: string
        is_active:
          type: boolean
        benefits:
          type: array
          items:
            $ref: '#/components/schemas/InsuranceBenefitWriteRequest'
        custom_data: {}
    PatchedNoteTemplateRequest:
      type: object
      description: Full CRUD serializer for clinical note templates.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        category:
          oneOf:
            - $ref: '#/components/schemas/CategoryEnum'
            - $ref: '#/components/schemas/BlankEnum'
        content:
          type: string
          minLength: 1
          description: >-
            Template body. Supports placeholder tokens like {{tooth}},
            {{provider}}.
        is_active:
          type: boolean
        sort_order:
          type: integer
          maximum: 2147483647
          minimum: 0
        custom_data: {}
    PatchedOperatoryRequest:
      type: object
      description: >-
        GH-711: ``provider_dentist`` / ``provider_hygienist`` must belong to URL
        practice.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        abbreviation:
          type: string
          maxLength: 20
        provider_dentist:
          type: string
          format: uuid
          nullable: true
          description: Default dentist assigned to this chair
        provider_hygienist:
          type: string
          format: uuid
          nullable: true
          description: Default hygienist assigned to this chair
        is_hygiene:
          type: boolean
        is_hidden:
          type: boolean
        is_bookable:
          type: boolean
          description: >-
            Operatories where is_bookable=False are skipped by the slot-finder.
            Used for PMS 'overflow' or 'notes-only' pseudo-chairs that host
            free-text annotations but aren't real rooms.
        sort_order:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Display ordering in schedule views
        is_web_sched:
          type: boolean
          description: Available for patient online scheduling
        custom_data: {}
    PatchedPatientAllergyRequest:
      type: object
      description: |-
        Full CRUD serializer for patient allergies.

        The ``patient`` field is read-only because it is set from the URL path.
        GH-711: bulk-injected ``patient`` is practice-validated.
      properties:
        allergy_name:
          type: string
          minLength: 1
          maxLength: 255
        reaction:
          type: string
          maxLength: 255
        is_active:
          type: boolean
        date_reaction:
          type: string
          format: date
          nullable: true
        custom_data: {}
    PatchedPatientConditionRequest:
      type: object
      description: |-
        Full CRUD serializer for patient medical conditions.

        The ``patient`` field is read-only because it is set from the URL path.
        GH-711: when the bulk view injects ``patient`` as writable, the
        inherited validator enforces practice match.
      properties:
        condition_name:
          type: string
          minLength: 1
          maxLength: 255
        date_start:
          type: string
          format: date
          nullable: true
        date_stop:
          type: string
          format: date
          nullable: true
        note:
          type: string
        custom_data: {}
    PatchedPatientEducationWriteRequest:
      type: object
      description: |-
        Write serializer for creating and updating patient education records.

        The ``patient`` field is excluded because it is set from the URL path
        by the view's ``perform_create`` method.
      properties:
        appointment:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        topic:
          $ref: '#/components/schemas/TopicEnum'
        detail:
          type: string
          description: Specific instructions or notes about what was discussed
        products_recommended:
          type: string
          description: e.g. Sonicare, Waterpik, MI Paste
          maxLength: 255
        education_date:
          type: string
          format: date
        compliance_note:
          type: string
          description: Patient response, compliance observations
        send_to_portal:
          type: boolean
          description: Make available in patient portal
        custom_data: {}
    PatchedPatientInsuranceBulkWriteRequest:
      type: object
      description: >-
        Write serializer for practice-wide bulk patient-insurance import.


        Differs from :class:`PatientInsuranceWriteSerializer` in two ways:


        1. ``patient`` is a writable required field (the practice-wide endpoint
           doesn't take ``patient_id`` from the URL, so each record must carry
           its own FK).
        2. If a record is POSTed with a ``carrier_name`` string but no
        ``carrier``
           FK, the serializer resolves-or-creates an :class:`InsuranceCarrier`
           row in the current practice scoped by name (case-insensitive) and
           links the enrollment to it.  This avoids the previous behavior
           where bulk imports created dangling enrollments with no carrier FK,
           and matches the semantics the sync tools expect: each PMS-level
           carrier string becomes exactly one ``InsuranceCarrier`` row.

        GH-711: the directly-supplied ``patient`` / ``insurance_plan`` /

        ``carrier`` / ``fee_schedule`` FKs are checked against the URL practice.

        The carrier-by-name resolver already scopes its lookup to the URL

        practice; only explicit UUID FK refs need validator coverage.
      properties:
        patient:
          type: string
          format: uuid
        insurance_plan:
          type: string
          format: uuid
          nullable: true
        carrier_name:
          type: string
          minLength: 1
          maxLength: 255
        carrier:
          type: string
          format: uuid
          nullable: true
        fee_schedule:
          type: string
          format: uuid
          nullable: true
          description: Fee schedule used for benefit estimation under this plan
        group_number:
          type: string
          maxLength: 100
        subscriber_id:
          type: string
          maxLength: 100
        subscriber_name:
          type: string
          description: Policy holder name if different from patient
          maxLength: 255
        ordinal:
          type: integer
          maximum: 32767
          minimum: 0
          description: 1=primary, 2=secondary
        relationship_to_subscriber:
          oneOf:
            - $ref: '#/components/schemas/RelationshipToSubscriberEnum'
            - $ref: '#/components/schemas/BlankEnum'
        effective_date:
          type: string
          format: date
          nullable: true
        termination_date:
          type: string
          format: date
          nullable: true
        custom_data: {}
    PatchedPatientInsuranceWriteRequest:
      type: object
      description: Write serializer for patient insurance enrollments.
      properties:
        insurance_plan:
          type: string
          format: uuid
          nullable: true
        carrier_name:
          type: string
          minLength: 1
          maxLength: 255
        carrier:
          type: string
          format: uuid
          nullable: true
        fee_schedule:
          type: string
          format: uuid
          nullable: true
          description: Fee schedule used for benefit estimation under this plan
        group_number:
          type: string
          maxLength: 100
        subscriber_id:
          type: string
          maxLength: 100
        subscriber_name:
          type: string
          description: Policy holder name if different from patient
          maxLength: 255
        ordinal:
          type: integer
          maximum: 32767
          minimum: 0
          description: 1=primary, 2=secondary
        relationship_to_subscriber:
          oneOf:
            - $ref: '#/components/schemas/RelationshipToSubscriberEnum'
            - $ref: '#/components/schemas/BlankEnum'
        effective_date:
          type: string
          format: date
          nullable: true
        termination_date:
          type: string
          format: date
          nullable: true
        custom_data: {}
    PatchedPatientMedicationRequest:
      type: object
      description: |-
        Full CRUD serializer for patient medications.

        The ``patient`` field is read-only because it is set from the URL path.
        GH-711: bulk-injected ``patient`` is practice-validated.
      properties:
        medication_name:
          type: string
          minLength: 1
          maxLength: 255
        date_start:
          type: string
          format: date
          nullable: true
        date_stop:
          type: string
          format: date
          nullable: true
        note:
          type: string
        rx_cui:
          type: string
          description: RxNorm concept unique identifier
          maxLength: 20
        custom_data: {}
    PatchedPatientRequest:
      type: object
      description: >-
        GH-711: ``primary_provider`` / ``secondary_provider`` FKs are
        practice-validated.
      properties:
        first_name:
          type: string
          minLength: 1
          maxLength: 100
        last_name:
          type: string
          minLength: 1
          maxLength: 100
        preferred_name:
          type: string
          maxLength: 100
        email:
          type: string
          format: email
          maxLength: 254
        date_of_birth:
          type: string
          format: date
          nullable: true
        gender:
          $ref: '#/components/schemas/GenderEnum'
        status:
          $ref: '#/components/schemas/PatientStatusEnum'
        phone_home:
          type: string
          nullable: true
        phone_work:
          type: string
          nullable: true
        phone_mobile:
          type: string
          nullable: true
        address:
          type: string
          maxLength: 255
        address2:
          type: string
          maxLength: 255
        city:
          type: string
          maxLength: 100
        state:
          type: string
          maxLength: 50
        zip_code:
          type: string
          pattern: ^\d{5}(-\d{4})?$
          maxLength: 10
        primary_provider:
          type: string
          format: uuid
          nullable: true
        secondary_provider:
          type: string
          format: uuid
          nullable: true
        balance_current:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: >-
            Current account balance, denormalized from the ledger for fast list
            rendering.
        last_recall_date:
          type: string
          format: date
          nullable: true
          description: Most recent recall appointment date from the source PMS
        next_recall_date:
          type: string
          format: date
          nullable: true
          description: Next scheduled recall appointment date from the source PMS
        prefers_email:
          type: boolean
          description: Patient opts in to email communications (per source PMS)
        prefers_sms:
          type: boolean
          description: Patient opts in to SMS/text communications (per source PMS)
        hipaa_consent_signed:
          type: boolean
          description: HIPAA treatment consent signed (per source PMS)
        hipaa_consent_date:
          type: string
          format: date
          nullable: true
          description: Date HIPAA treatment consent was signed
        hipaa_authorization_signed:
          type: boolean
          description: HIPAA authorization (release of info) signed (per source PMS)
        hipaa_authorization_date:
          type: string
          format: date
          nullable: true
          description: Date HIPAA authorization was signed
        hipaa_privacy_practices_signed:
          type: boolean
          description: HIPAA notice of privacy practices acknowledged (per source PMS)
        hipaa_privacy_practices_date:
          type: string
          format: date
          nullable: true
          description: Date HIPAA notice of privacy practices was acknowledged
        custom_data: {}
    PatchedPaymentPlanWriteRequest:
      type: object
      description: >-
        Write serializer for creating a payment plan with nested charges.


        Practice and patient are set from the URL path by the view.

        Accepts an optional ``charges`` array to create charges in a single
        request.


        GH-711: ``guarantor`` (Patient FK) and the injected ``patient`` FK on

        the bulk path are validated against the URL practice.
      properties:
        guarantor:
          type: string
          format: uuid
          nullable: true
        plan_date:
          type: string
          format: date
          nullable: true
        apr:
          type: string
          format: decimal
          pattern: ^-?\d{0,3}(?:\.\d{0,2})?$
        completed_amt:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        is_closed:
          type: boolean
        charges:
          type: array
          items:
            $ref: '#/components/schemas/PaymentPlanChargeWriteRequest'
        custom_data: {}
    PatchedPaymentRecordWriteRequest:
      type: object
      description: |-
        Create a payment record with optional nested splits.

        Practice and patient are set by the view.
      properties:
        pay_date:
          type: string
          format: date
          nullable: true
        pay_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        pay_type:
          type: string
          maxLength: 100
        pay_note:
          type: string
        provider:
          type: string
          format: uuid
          nullable: true
        custom_data: {}
        splits:
          type: array
          items:
            $ref: '#/components/schemas/PaymentSplitWriteRequest'
    PatchedPerioExamWriteRequest:
      type: object
      description: |-
        Write serializer for creating and updating perio exams.

        The ``patient`` field is excluded because it is set from the URL path
        by the view's ``perform_create`` method.

        GH-711: ``patient`` / ``provider`` / ``appointment`` FKs are
        practice-validated.
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        appointment:
          type: string
          format: uuid
          nullable: true
        exam_date:
          type: string
          format: date-time
        is_completed:
          type: boolean
          description: Completed exams cannot be deleted
        stage:
          oneOf:
            - $ref: '#/components/schemas/StageEnum'
            - $ref: '#/components/schemas/BlankEnum'
        grade:
          oneOf:
            - $ref: '#/components/schemas/GradeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        note:
          type: string
        custom_data: {}
    PatchedPracticeMemberUpdateRequest:
      type: object
      description: >-
        Write serializer for updating a practice membership (role + active
        status).
      properties:
        role:
          $ref: '#/components/schemas/MemberRoleEnum'
        is_active:
          type: boolean
    PatchedPracticeRequest:
      type: object
      description: Full representation of a dental practice.
      properties:
        client:
          type: string
          format: uuid
          nullable: true
        name:
          type: string
          minLength: 1
          maxLength: 255
        address_line1:
          type: string
          maxLength: 255
        address_line2:
          type: string
          maxLength: 255
        city:
          type: string
          maxLength: 100
        state:
          type: string
          maxLength: 2
        zip_code:
          type: string
          pattern: ^\d{5}(-\d{4})?$
          maxLength: 10
        phone:
          type: string
          maxLength: 128
        timezone:
          type: string
          minLength: 1
          maxLength: 50
        settings:
          description: Practice-specific configuration.
        production_goal_daily:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Daily production goal in USD. Null = no goal set.
        production_goal_weekly:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Weekly production goal in USD. Null = no goal set.
        production_goal_monthly:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Monthly production goal in USD. Null = no goal set.
        is_active:
          type: boolean
        custom_data: {}
    PatchedPreferenceRequest:
      type: object
      description: Exposes preference data without user/application fields.
      properties:
        data: {}
    PatchedPrescriptionRequest:
      type: object
      description: >-
        Prescription serializer.


        ``patient_name`` and ``provider_name`` are pre-joined read-only fields
        for

        display without a second round-trip.  ``is_controlled`` is a derived

        boolean that the web form uses to trigger the PDMP reminder.
      properties:
        patient:
          type: string
          format: uuid
        provider:
          type: string
          format: uuid
          nullable: true
          description: The prescribing/responsible provider. Required to finalise.
        drug_name:
          type: string
          minLength: 1
          description: e.g. Amoxicillin
          maxLength: 200
        drug_strength:
          type: string
          description: e.g. 500mg
          maxLength: 100
        drug_form:
          type: string
          description: 'Dosage form: Capsule, Tablet, Solution, Cream, etc.'
          maxLength: 100
        quantity:
          type: string
          description: 'e.g. #21 or 30 mL'
          maxLength: 50
        directions:
          type: string
          minLength: 1
          description: >-
            Sig / patient instructions e.g. 'Take 1 capsule by mouth three times
            daily for 7 days'
        refills:
          type: integer
          maximum: 32767
          minimum: 0
          description: >-
            Number of refills (0 = no refills). Controlled substances may not be
            refilled in many states.
        substitution_permitted:
          type: boolean
          description: Allow pharmacist to substitute generic equivalent.
        cs_schedule:
          allOf:
            - $ref: '#/components/schemas/CsScheduleEnum'
          description: >-
            DEA Controlled Substance Schedule. Non-Controlled for the vast
            majority of dental prescriptions (antibiotics, NSAIDs, fluoride,
            chlorhexidine). Use Schedule II–V only for opioids, benzodiazepines,
            etc.


            * `none` - Non-Controlled

            * `II` - Schedule II

            * `III` - Schedule III

            * `IV` - Schedule IV

            * `V` - Schedule V
        diagnosis_description:
          type: string
          description: Brief clinical indication e.g. 'Post-extraction infection'.
          maxLength: 255
        appointment:
          type: string
          format: uuid
          nullable: true
        notes:
          type: string
    PatchedPrescriptionTemplateRequest:
      type: object
      description: Read/write serializer for practice prescription templates.
      properties:
        name:
          type: string
          minLength: 1
          description: >-
            Template display name shown in the picker e.g. 'Amoxicillin 500mg
            #21'
          maxLength: 200
        drug_name:
          type: string
          minLength: 1
          maxLength: 200
        drug_strength:
          type: string
          maxLength: 100
        drug_form:
          type: string
          maxLength: 100
        quantity:
          type: string
          maxLength: 50
        directions:
          type: string
          minLength: 1
        refills:
          type: integer
          maximum: 32767
          minimum: 0
        substitution_permitted:
          type: boolean
        cs_schedule:
          $ref: '#/components/schemas/CsScheduleEnum'
        is_active:
          type: boolean
    PatchedProcedureCodeRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        code:
          type: string
          minLength: 1
          maxLength: 20
        description:
          type: string
          maxLength: 255
        abbreviation:
          type: string
          maxLength: 100
        service_type:
          oneOf:
            - $ref: '#/components/schemas/ServiceTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        default_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          description: Practice UCR fee
        default_duration_minutes:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Default appointment block size
        generate_recall:
          type: boolean
          description: Triggers recall scheduling when completed
        is_active:
          type: boolean
        ada_code:
          type: string
          description: ADA/CDT code reference (insurance module)
          maxLength: 20
        custom_data: {}
    PatchedProcedureRequest:
      type: object
      description: |-
        Read serializer for procedures with denormalized provider name.

        Exposes both the legacy string ``proc_code`` (authoritative for PMS
        imports where no canonical ProcedureCode match exists) and the nested
        ``procedure_code`` FK object (populated when the resolver found a
        matching :class:`ProcedureCode` in the practice).
      properties:
        custom_data: {}
    PatchedProcedureWriteRequest:
      type: object
      description: >-
        Write serializer for creating and updating procedures.


        The ``patient`` field is excluded because it is set from the URL path

        by the view's ``perform_create`` method.


        On ``create``/``update``, resolves the ``proc_code`` string into the

        ``procedure_code`` FK (practice-scoped) and backfills ``proc_fee`` from

        ``ProcedureCode.default_fee`` when the caller sent no non-zero fee.

        Without this, PMS imports (which send only the string) leave the FK
        NULL.

        See :func:`_resolve_procedure_code_fk` for the shared logic.


        GH-711: ``patient`` / ``provider`` / ``appointment`` /
        ``procedure_code``

        FKs are validated against the URL practice via

        :class:`apps.common.bulk.PracticeFKValidatorMixin`.  Used directly by

        :class:`apps.sync.views_bulk.BulkProcedureView` (with ``patient``

        injected by ``make_bulk_serializer``).
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        appointment:
          type: string
          format: uuid
          nullable: true
        procedure_code:
          type: string
          format: uuid
          nullable: true
        proc_code:
          type: string
          maxLength: 20
        description:
          type: string
          maxLength: 255
        proc_date:
          type: string
          format: date
          nullable: true
        proc_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        status:
          type: string
          description: complete, existing_current, existing_other
          maxLength: 30
        tooth_num:
          type: string
          maxLength: 10
        surface:
          type: string
          maxLength: 20
        discount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        unit_qty:
          type: integer
          maximum: 2147483647
          minimum: 0
        custom_data: {}
    PatchedProviderRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        first_name:
          type: string
          minLength: 1
          maxLength: 100
        last_name:
          type: string
          minLength: 1
          maxLength: 100
        abbreviation:
          type: string
          maxLength: 20
        specialty:
          type: string
          maxLength: 100
        provider_type:
          $ref: '#/components/schemas/ProviderTypeEnum'
        color:
          type: string
          description: 'Hex color for calendar rendering, e.g. #3B82F6'
          maxLength: 7
        npi:
          type: string
          description: National Provider Identifier
          maxLength: 10
        dea_number:
          type: string
          description: >-
            DEA registration number (2 letters + 7 digits, e.g. AB1234567). 
            Required on paper controlled-substance prescriptions.  Not required
            for non-controlled prescriptions.  Added as part of the
            prescriptions module (escape#136 / api#482).
          maxLength: 9
        is_hidden:
          type: boolean
          description: Hidden from dropdowns but visible on historical records
        is_secondary:
          type: boolean
          description: Legacy import field — use provider_type instead
        is_active:
          type: boolean
          description: Currently employed at this practice
        is_web_sched:
          type: boolean
          description: Available for patient online scheduling.
        custom_data: {}
    PatchedRecallRequest:
      type: object
      description: >-
        Read serializer for Recall / Recare records.


        ``last_visit_date``, ``has_future_appointment`` and ``last_reached_at``

        are populated by annotations added in

        :class:`apps.scheduling.views.RecallListCreateView` (and the
        patient-scoped

        variant).  When a recall is serialized from a queryset that does not

        annotate these fields they fall back to ``None``.
      properties:
        custom_data: {}
    PatchedRecallWriteRequest:
      type: object
      description: |-
        Write serializer for Recall / Recare records.

        GH-711: ``patient`` FK must belong to URL practice on the bulk path.
      properties:
        patient:
          type: string
          format: uuid
        recall_type:
          type: string
          minLength: 1
          maxLength: 100
        date_due:
          type: string
          format: date
          nullable: true
        date_previous:
          type: string
          format: date
          nullable: true
        date_scheduled:
          type: string
          format: date
          nullable: true
        is_disabled:
          type: boolean
        custom_data: {}
    PatchedReferralSourceRequest:
      type: object
      description: >-
        Read+write serializer for referral sources.


        Used for both list/retrieve and create/update. Practice is set by the
        view.

        ``custom_data`` is scoped per OAuth application.
      properties:
        first_name:
          type: string
          maxLength: 100
        last_name:
          type: string
          maxLength: 100
        specialty:
          type: string
          maxLength: 100
        phone:
          type: string
          maxLength: 20
        email:
          type: string
          format: email
          maxLength: 254
        npi:
          type: string
          maxLength: 20
        is_doctor:
          type: boolean
        custom_data: {}
    PatchedReferralSourceWriteRequest:
      type: object
      description: |-
        Write serializer for creating referral sources.

        Practice is set from the URL path by the view.
      properties:
        first_name:
          type: string
          maxLength: 100
        last_name:
          type: string
          maxLength: 100
        specialty:
          type: string
          maxLength: 100
        phone:
          type: string
          maxLength: 20
        email:
          type: string
          format: email
          maxLength: 254
        npi:
          type: string
          maxLength: 20
        is_doctor:
          type: boolean
        custom_data: {}
    PatchedScheduleOverrideRequest:
      type: object
      description: 'GH-711: ``provider`` / ``operatory`` FKs must belong to URL practice.'
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        schedule_date:
          type: string
          format: date
        start_time:
          type: string
          format: time
        end_time:
          type: string
          format: time
        schedule_type:
          $ref: '#/components/schemas/ScheduleTypeEnum'
        blockout_type:
          type: string
          description: e.g. lunch, meeting, holiday, time_off
          maxLength: 50
        operatory:
          type: string
          format: uuid
          nullable: true
        status:
          $ref: '#/components/schemas/ScheduleOverrideStatusEnum'
        note:
          type: string
        custom_data: {}
    PatchedScheduleTemplateRequest:
      type: object
      description: 'GH-711: ``provider`` / ``operatory`` FKs must belong to URL practice.'
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        day_of_week:
          type: integer
          maximum: 2147483647
          minimum: -2147483648
          description: 0=Sunday, 1=Monday, ..., 6=Saturday
        start_time:
          type: string
          format: time
        end_time:
          type: string
          format: time
        schedule_type:
          $ref: '#/components/schemas/ScheduleTypeEnum'
        blockout_type:
          type: string
          description: e.g. lunch, meeting, admin
          maxLength: 50
        operatory:
          type: string
          format: uuid
          nullable: true
        is_active:
          type: boolean
        custom_data: {}
    PatchedSourceRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        source_type:
          $ref: '#/components/schemas/SourceTypeEnum'
        is_active:
          type: boolean
        notes:
          type: string
        custom_data: {}
    PatchedToothConditionRequest:
      type: object
      description: |-
        Full CRUD serializer for tooth conditions (odontogram).

        The ``patient`` field is read-only because it is set from the URL path.
        GH-711: bulk-injected ``patient`` is practice-validated.
      properties:
        tooth_num:
          type: string
          minLength: 1
          description: 'Universal numbering: 1-32 (permanent), A-T (primary)'
          maxLength: 5
        condition:
          $ref: '#/components/schemas/ConditionEnum'
        dentition:
          $ref: '#/components/schemas/DentitionEnum'
        surfaces:
          type: string
          description: 'Affected surfaces: M, O, D, B/F, L, I (e.g. ''MOD'')'
          maxLength: 10
        material:
          type: string
          description: e.g. PFM, zirconia, amalgam, composite
          maxLength: 50
        note:
          type: string
        date_identified:
          type: string
          format: date
          nullable: true
        is_current:
          type: boolean
          description: False = historical condition superseded by a newer one
        custom_data: {}
    PatchedTreatmentPlanItemRequest:
      type: object
      description: >-
        Full CRUD serializer for treatment plan items.


        The ``patient`` and ``practice`` fields are read-only because they are
        set

        from the URL path by the view's ``perform_create`` method.


        Exposes ``procedure_code_fee`` (denormalized from the linked
        ProcedureCode's

        ``default_fee``) so the UI can show the practice's UCR fee even when the

        item's own ``planned_fee`` is 0 (e.g. fresh imports that haven't run

        through the estimate flow yet).
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        procedure_code:
          type: string
          format: uuid
          nullable: true
        proc_code:
          type: string
          maxLength: 20
        description:
          type: string
          maxLength: 255
        tooth_num:
          type: string
          maxLength: 10
        surface:
          type: string
          maxLength: 20
        planned_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        date_planned:
          type: string
          format: date
          nullable: true
        status:
          type: string
          description: planned, completed
          maxLength: 30
        plan_name:
          type: string
          maxLength: 255
        priority:
          type: integer
          maximum: 2147483647
          minimum: 0
        custom_data: {}
    PatchedTreatmentPlanWriteRequest:
      type: object
      description: >-
        Write serializer for creating and updating treatment plans.


        The ``patient`` field is excluded because it is set from the URL path.

        Supports nested ``items`` for creating a plan with line items in one
        request.


        GH-711: ``patient`` / ``provider`` FKs are practice-validated.
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        name:
          type: string
          minLength: 1
          maxLength: 200
        note:
          type: string
        status:
          $ref: '#/components/schemas/TreatmentPlanStatusEnum'
        date_accepted:
          type: string
          format: date
          nullable: true
        custom_data: {}
        items:
          type: array
          items:
            $ref: '#/components/schemas/TreatmentPlanItemWriteRequest'
    PatchedUserRequest:
      type: object
      description: |-
        Public representation of a user (excludes sensitive fields).

        ``is_staff`` and ``is_superuser`` are exposed read-only so client apps
        (notably the internal support tool) can gate staff-only / superuser-only
        UI on the authenticated user's own ``/me/`` response, and so the support
        tool's Staff tab can distinguish the two populations in its list view.
        Both fields are in ``read_only_fields`` so a regular PATCH cannot
        escalate privileges.

        ``current_practice_id`` (read-only) is populated when the request is
        authenticated via a ServiceToken — the long-lived OAuth refresh token
        issued to the importer / sync tools is bound to a single practice at
        consent time, and surfacing that binding lets clients drop their own
        multi-practice fallback logic. ``null`` for password-grant or any
        non-service-token authenticated request.

        ``practice_timezone`` (read-only) is the IANA timezone string of the
        practice this session is anchored to. The patient portal uses it to
        compute "today" / "tomorrow" against the office's calendar instead of
        the browser's — a remote staff member or a patient travelling out of
        state must still see the practice's perspective (per saved memory
        ``feedback_practice_tz_not_browser``). Resolution order:

        1. Service-token-bound practice (matches ``current_practice_id``).
        2. The user's first active ``PracticeMembership`` (covers the common
           patient-portal case where a portal account has exactly one
           membership, and the staff-token case where ``current_practice_id``
           is null but the user still has a single anchor).
        3. ``null`` when the user has no active membership at all.
      properties:
        first_name:
          type: string
          minLength: 1
          maxLength: 150
        last_name:
          type: string
          minLength: 1
          maxLength: 150
    PatchedVitalSignWriteRequest:
      type: object
      description: |-
        Write serializer for creating and updating vital sign readings.

        The ``patient`` field is excluded because it is set from the URL path
        by the view's ``perform_create`` method.

        GH-711: ``patient`` / ``appointment`` / ``provider`` FKs are
        practice-validated.
      properties:
        appointment:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        reading_date:
          type: string
          format: date-time
          description: When the vitals were taken
        bp_systolic:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Systolic blood pressure (mmHg)
        bp_diastolic:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Diastolic blood pressure (mmHg)
        pulse:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Pulse rate (bpm)
        respiration:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Respiration rate (breaths/min)
        o2_saturation:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Oxygen saturation (%)
        temperature:
          type: string
          format: decimal
          pattern: ^-?\d{0,3}(?:\.\d{0,1})?$
          nullable: true
          description: Temperature (F)
        note:
          type: string
        custom_data: {}
    PatchedWebhookSubscriptionCreateRequest:
      type: object
      description: >-
        Write serializer for creating/updating webhook subscriptions — validates
        URL and event patterns.
      properties:
        application:
          type: integer
        client:
          type: string
          format: uuid
        target_url:
          type: string
          format: uri
          minLength: 1
          maxLength: 2048
        events:
          description: List of event patterns, e.g. ['patient.created', 'appointment.*']
        all_practices:
          type: boolean
          description: Subscribe to events across all practices, or select specific ones
        practices:
          type: array
          items:
            type: string
            format: uuid
          description: Specific practices (when all_practices=False)
        custom_data: {}
    Patched_MessagingSettingsRequest:
      type: object
      description: |-
        Read/write payload for the per-practice messaging settings.

        On read we return both the raw ID list (the canonical setting) and
        a derived ``inbox_owner_users`` array of compact envelopes so the
        UI can render the chips without a second round-trip.  On write the
        caller PATCHes only ``inbox_owner_user_ids`` -- the derived
        envelopes are read-only.
      properties:
        inbox_owner_user_ids:
          type: array
          items:
            type: string
            format: uuid
          description: >-
            Users to route new patient-portal threads to.  Empty list restores
            the historical ADMIN / OFFICE_MANAGER fan-out.
    Patient:
      type: object
      description: >-
        GH-711: ``primary_provider`` / ``secondary_provider`` FKs are
        practice-validated.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        first_name:
          type: string
          maxLength: 100
        last_name:
          type: string
          maxLength: 100
        preferred_name:
          type: string
          maxLength: 100
        email:
          type: string
          format: email
          maxLength: 254
        date_of_birth:
          type: string
          format: date
          nullable: true
        gender:
          $ref: '#/components/schemas/GenderEnum'
        status:
          $ref: '#/components/schemas/PatientStatusEnum'
        phone_home:
          type: string
          nullable: true
        phone_work:
          type: string
          nullable: true
        phone_mobile:
          type: string
          nullable: true
        address:
          type: string
          maxLength: 255
        address2:
          type: string
          maxLength: 255
        city:
          type: string
          maxLength: 100
        state:
          type: string
          maxLength: 50
        zip_code:
          type: string
          pattern: ^\d{5}(-\d{4})?$
          maxLength: 10
        primary_provider:
          type: string
          format: uuid
          nullable: true
        secondary_provider:
          type: string
          format: uuid
          nullable: true
        last_medical_review:
          type: string
          format: date
          readOnly: true
          nullable: true
          description: Date medical history was last reviewed with patient
        last_medical_review_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff member who last reviewed medical history
        last_medical_review_by_name:
          type: string
          nullable: true
          description: >-
            Return the display name of the user who last reviewed medical
            history.
          readOnly: true
        balance_current:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: >-
            Current account balance, denormalized from the ledger for fast list
            rendering.
        last_recall_date:
          type: string
          format: date
          nullable: true
          description: Most recent recall appointment date from the source PMS
        next_recall_date:
          type: string
          format: date
          nullable: true
          description: Next scheduled recall appointment date from the source PMS
        prefers_email:
          type: boolean
          description: Patient opts in to email communications (per source PMS)
        prefers_sms:
          type: boolean
          description: Patient opts in to SMS/text communications (per source PMS)
        hipaa_consent_signed:
          type: boolean
          description: HIPAA treatment consent signed (per source PMS)
        hipaa_consent_date:
          type: string
          format: date
          nullable: true
          description: Date HIPAA treatment consent was signed
        hipaa_authorization_signed:
          type: boolean
          description: HIPAA authorization (release of info) signed (per source PMS)
        hipaa_authorization_date:
          type: string
          format: date
          nullable: true
          description: Date HIPAA authorization was signed
        hipaa_privacy_practices_signed:
          type: boolean
          description: HIPAA notice of privacy practices acknowledged (per source PMS)
        hipaa_privacy_practices_date:
          type: string
          format: date
          nullable: true
          description: Date HIPAA notice of privacy practices was acknowledged
        custom_data: {}
        referral_source:
          type: string
          nullable: true
          description: >-
            Return ``"Last, First"`` for the most recent incoming referral
            source, or None.


            Reads the fields annotated by :class:`PatientListView`.  When the

            serializer is called in a context that didn't annotate them

            (e.g. ``PatientDetailView``), the getters return ``None`` which

            surfaces here as "no referral on file."
          readOnly: true
      required:
        - first_name
        - id
        - last_medical_review
        - last_medical_review_by
        - last_medical_review_by_name
        - last_name
        - referral_source
    PatientAllergy:
      type: object
      description: |-
        Full CRUD serializer for patient allergies.

        The ``patient`` field is read-only because it is set from the URL path.
        GH-711: bulk-injected ``patient`` is practice-validated.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        allergy_name:
          type: string
          maxLength: 255
        reaction:
          type: string
          maxLength: 255
        is_active:
          type: boolean
        date_reaction:
          type: string
          format: date
          nullable: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - allergy_name
        - created
        - id
        - modified
        - patient
    PatientAllergyRequest:
      type: object
      description: |-
        Full CRUD serializer for patient allergies.

        The ``patient`` field is read-only because it is set from the URL path.
        GH-711: bulk-injected ``patient`` is practice-validated.
      properties:
        allergy_name:
          type: string
          minLength: 1
          maxLength: 255
        reaction:
          type: string
          maxLength: 255
        is_active:
          type: boolean
        date_reaction:
          type: string
          format: date
          nullable: true
        custom_data: {}
      required:
        - allergy_name
    PatientBalance:
      type: object
      description: Read-only serializer for patient balance snapshots.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        custom_data:
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        balance_total:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        insurance_estimate:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        patient_portion:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        created_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        modified_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        practice:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
      required:
        - balance_total
        - created
        - created_by
        - custom_data
        - id
        - insurance_estimate
        - modified
        - modified_by
        - patient
        - patient_portion
        - practice
    PatientBalanceReportResponse:
      type: object
      description: Paginated response from ``GET /reports/patient-balance/``.
      properties:
        count:
          type: integer
        next:
          type: string
          nullable: true
        previous:
          type: string
          nullable: true
        results:
          type: array
          items:
            $ref: '#/components/schemas/PatientBalanceReportRow'
        summary:
          $ref: '#/components/schemas/AgingReportSummary'
      required:
        - count
        - next
        - previous
        - results
        - summary
    PatientBalanceReportRow:
      type: object
      description: One row in the patient-balance report.
      properties:
        patient_id:
          type: string
        patient_name:
          type: string
        balance_total:
          type: string
        bal_0_30:
          type: string
        bal_31_60:
          type: string
        bal_61_90:
          type: string
        bal_over_90:
          type: string
        last_payment_date:
          type: string
          nullable: true
        last_payment_amount:
          type: string
          nullable: true
        primary_provider_id:
          type: string
          nullable: true
        primary_provider_name:
          type: string
        primary_carrier_id:
          type: string
          nullable: true
        primary_carrier_name:
          type: string
        status:
          type: string
      required:
        - bal_0_30
        - bal_31_60
        - bal_61_90
        - bal_over_90
        - balance_total
        - last_payment_amount
        - last_payment_date
        - patient_id
        - patient_name
    PatientBookingRequest:
      type: object
      description: |-
        Validates and creates a patient-initiated appointment booking.

        The patient is resolved from the authenticated user's email matched
        against Patient records in the practice. Provider and operatory are
        validated to belong to the practice.

        Procedure codes are matched by CDT code string (e.g. "D0120") against
        the practice's ProcedureCode reference table.
      properties:
        provider:
          type: string
          format: uuid
          description: Provider UUID
        operatory:
          type: string
          format: uuid
          description: Operatory UUID
        start_time:
          type: string
          format: date-time
          description: Appointment start (ISO 8601 with timezone)
        duration_minutes:
          type: integer
          maximum: 480
          minimum: 5
          description: Duration in minutes
        procedures:
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 20
          description: List of CDT procedure codes (e.g. ['D0120', 'D1110'])
        notes:
          type: string
          default: ''
          maxLength: 2000
      required:
        - duration_minutes
        - operatory
        - provider
        - start_time
    PatientBookingResponse:
      type: object
      description: Read-only response serializer for a patient-created appointment.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        provider:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        operatory:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        date_time:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        duration_minutes:
          type: integer
          readOnly: true
        status:
          allOf:
            - $ref: '#/components/schemas/AppointmentStatusEnum'
          readOnly: true
        note:
          type: string
          readOnly: true
        provider_name:
          type: string
          readOnly: true
      required:
        - date_time
        - duration_minutes
        - id
        - note
        - operatory
        - provider
        - provider_name
        - status
    PatientCancellationRequest:
      type: object
      description: Payload for a patient-initiated appointment cancellation.
      properties:
        reason:
          type: string
          default: ''
          description: Patient-supplied reason for cancellation.
          maxLength: 2000
        reschedule_request:
          type: boolean
          default: false
          description: If true, the front desk is notified to reschedule.
    PatientCancellationResponse:
      type: object
      description: Read-only response for a cancelled appointment.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        provider:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        operatory:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        date_time:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        duration_minutes:
          type: integer
          readOnly: true
        status:
          allOf:
            - $ref: '#/components/schemas/AppointmentStatusEnum'
          readOnly: true
        cancellation_reason:
          type: string
          readOnly: true
          description: Patient-supplied reason for cancellation
        reschedule_requested:
          type: boolean
          readOnly: true
          description: Patient asked the front desk to reschedule
        provider_name:
          type: string
          readOnly: true
      required:
        - cancellation_reason
        - date_time
        - duration_minutes
        - id
        - operatory
        - provider
        - provider_name
        - reschedule_requested
        - status
    PatientCondition:
      type: object
      description: |-
        Full CRUD serializer for patient medical conditions.

        The ``patient`` field is read-only because it is set from the URL path.
        GH-711: when the bulk view injects ``patient`` as writable, the
        inherited validator enforces practice match.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        condition_name:
          type: string
          maxLength: 255
        date_start:
          type: string
          format: date
          nullable: true
        date_stop:
          type: string
          format: date
          nullable: true
        note:
          type: string
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - condition_name
        - created
        - id
        - modified
        - patient
    PatientConditionRequest:
      type: object
      description: |-
        Full CRUD serializer for patient medical conditions.

        The ``patient`` field is read-only because it is set from the URL path.
        GH-711: when the bulk view injects ``patient`` as writable, the
        inherited validator enforces practice match.
      properties:
        condition_name:
          type: string
          minLength: 1
          maxLength: 255
        date_start:
          type: string
          format: date
          nullable: true
        date_stop:
          type: string
          format: date
          nullable: true
        note:
          type: string
        custom_data: {}
      required:
        - condition_name
    PatientEducation:
      type: object
      description: >-
        Read serializer for patient education records with denormalized display
        fields.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        appointment:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        provider:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        provider_name:
          type: string
          nullable: true
          description: Return the provider's display name, or None if no provider is set.
          readOnly: true
        topic:
          allOf:
            - $ref: '#/components/schemas/TopicEnum'
          readOnly: true
        topic_display:
          type: string
          description: Return the human-readable topic label.
          readOnly: true
        detail:
          type: string
          readOnly: true
          description: Specific instructions or notes about what was discussed
        products_recommended:
          type: string
          readOnly: true
          description: e.g. Sonicare, Waterpik, MI Paste
        education_date:
          type: string
          format: date
          readOnly: true
        compliance_note:
          type: string
          readOnly: true
          description: Patient response, compliance observations
        send_to_portal:
          type: boolean
          readOnly: true
          description: Make available in patient portal
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - appointment
        - compliance_note
        - created
        - detail
        - education_date
        - id
        - modified
        - patient
        - products_recommended
        - provider
        - provider_name
        - send_to_portal
        - topic
        - topic_display
    PatientEducationWrite:
      type: object
      description: |-
        Write serializer for creating and updating patient education records.

        The ``patient`` field is excluded because it is set from the URL path
        by the view's ``perform_create`` method.
      properties:
        appointment:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        topic:
          $ref: '#/components/schemas/TopicEnum'
        detail:
          type: string
          description: Specific instructions or notes about what was discussed
        products_recommended:
          type: string
          description: e.g. Sonicare, Waterpik, MI Paste
          maxLength: 255
        education_date:
          type: string
          format: date
        compliance_note:
          type: string
          description: Patient response, compliance observations
        send_to_portal:
          type: boolean
          description: Make available in patient portal
        custom_data: {}
      required:
        - education_date
        - topic
    PatientEducationWriteRequest:
      type: object
      description: |-
        Write serializer for creating and updating patient education records.

        The ``patient`` field is excluded because it is set from the URL path
        by the view's ``perform_create`` method.
      properties:
        appointment:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        topic:
          $ref: '#/components/schemas/TopicEnum'
        detail:
          type: string
          description: Specific instructions or notes about what was discussed
        products_recommended:
          type: string
          description: e.g. Sonicare, Waterpik, MI Paste
          maxLength: 255
        education_date:
          type: string
          format: date
        compliance_note:
          type: string
          description: Patient response, compliance observations
        send_to_portal:
          type: boolean
          description: Make available in patient portal
        custom_data: {}
      required:
        - education_date
        - topic
    PatientFeedbackCreateRequest:
      type: object
      description: |-
        Validate and persist an inbound patient-feedback submission.

        The portal posts JSON only -- there's deliberately no multipart
        branch because we don't accept screenshots from patients (see the
        model docstring for why).  The serializer accepts the three
        user-supplied fields plus two optional context fields; everything
        else (``user``, ``patient``, ``practice``, ``client``, status,
        triage stamps) is set server-side.
      properties:
        title:
          type: string
          minLength: 1
          description: One-line summary the patient typed into the feedback dialog.
          maxLength: 80
        details:
          type: string
          minLength: 1
          description: Long-form description of the issue.
        page_url:
          type: string
          description: Page URL the feedback was submitted from.
          maxLength: 2000
        viewport:
          description: Browser viewport metadata (width / height / devicePixelRatio).
        user_agent:
          type: string
          description: User-Agent string at submission time.
          maxLength: 1024
      required:
        - details
        - title
    PatientFeedbackDetail:
      type: object
      description: |-
        Full read shape for a single :class:`PatientFeedback` row.

        All fields are read-only.  Status changes go through
        :class:`apps.patient_feedback.views.PatientFeedbackTransitionView`
        so the ``triaged_by`` / ``triaged_at`` / ``resolved_by`` /
        ``resolved_at`` side effects fire consistently.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        user:
          type: string
          format: uuid
          readOnly: true
          description: >-
            Patient portal user who submitted the feedback (set server-side from
            request.user).
        submitter_email:
          type: string
          format: email
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: >-
            Matched patient record at submit time, when known (resolved by
            practice + email).
        patient_name:
          type: string
          nullable: true
          readOnly: true
        practice:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Practice context at submission time, when known.
        client:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Client that owns the practice; auto-derived from practice on save.
        title:
          type: string
          readOnly: true
          description: One-line summary the patient typed into the feedback dialog.
        details:
          type: string
          readOnly: true
          description: Long-form description of the issue.
        page_url:
          type: string
          readOnly: true
          description: Page URL the feedback was submitted from.
        viewport:
          readOnly: true
          description: Browser viewport metadata (width / height / devicePixelRatio).
        user_agent:
          type: string
          readOnly: true
          description: User-Agent string at submission time.
        status:
          allOf:
            - $ref: '#/components/schemas/FeedbackStatusEnum'
          readOnly: true
          description: |-
            Triage state.

            * `new` - New
            * `triaged` - Triaged
            * `resolved` - Resolved
        triaged_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Internal staff user who first moved this row out of 'new'.
        triaged_by_email:
          type: string
          format: email
          readOnly: true
        triaged_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When the row was first moved out of 'new'.
        resolved_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Internal staff user who marked this row resolved.
        resolved_by_email:
          type: string
          format: email
          readOnly: true
        resolved_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When the row was marked resolved.
        resolution_note:
          type: string
          readOnly: true
          description: Free-form note attached on a status transition.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - client
        - created
        - details
        - id
        - modified
        - page_url
        - patient
        - patient_name
        - practice
        - resolution_note
        - resolved_at
        - resolved_by
        - resolved_by_email
        - status
        - submitter_email
        - title
        - triaged_at
        - triaged_by
        - triaged_by_email
        - user
        - user_agent
        - viewport
    PatientFeedbackList:
      type: object
      description: |-
        Compact list shape for the internal-staff triage inbox.

        ``viewport`` and ``details`` are omitted to keep list payloads
        small; open the detail view to see them.  ``submitter_email`` /
        ``patient_name`` are denormalised so the inbox UI doesn't have to
        round-trip through the user / patient endpoints for every row.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        user:
          type: string
          format: uuid
          readOnly: true
          description: >-
            Patient portal user who submitted the feedback (set server-side from
            request.user).
        submitter_email:
          type: string
          format: email
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: >-
            Matched patient record at submit time, when known (resolved by
            practice + email).
        patient_name:
          type: string
          nullable: true
          description: |-
            Return the matched patient's display name, or ``None``.

            Read off the FK; falls back gracefully when the patient row was
            deleted after the feedback was filed (``on_delete=SET_NULL``
            leaves the FK pointing at no row).
          readOnly: true
        practice:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Practice context at submission time, when known.
        client:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Client that owns the practice; auto-derived from practice on save.
        title:
          type: string
          readOnly: true
          description: One-line summary the patient typed into the feedback dialog.
        page_url:
          type: string
          readOnly: true
          description: Page URL the feedback was submitted from.
        user_agent:
          type: string
          readOnly: true
          description: User-Agent string at submission time.
        status:
          allOf:
            - $ref: '#/components/schemas/FeedbackStatusEnum'
          readOnly: true
          description: |-
            Triage state.

            * `new` - New
            * `triaged` - Triaged
            * `resolved` - Resolved
        triaged_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Internal staff user who first moved this row out of 'new'.
        triaged_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When the row was first moved out of 'new'.
        resolved_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Internal staff user who marked this row resolved.
        resolved_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When the row was marked resolved.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - client
        - created
        - id
        - modified
        - page_url
        - patient
        - patient_name
        - practice
        - resolved_at
        - resolved_by
        - status
        - submitter_email
        - title
        - triaged_at
        - triaged_by
        - user
        - user_agent
    PatientFeedbackTransitionRequest:
      type: object
      description: |-
        Body shape for the internal-staff status-transition endpoint.

        Two writable fields:

        * ``status`` -- target :class:`PatientFeedbackStatus` value.
          Required.
        * ``resolution_note`` -- optional free-form text attached to the
          transition; surfaces in the detail view.  Defaults to empty
          string so existing notes are preserved if the caller omits the
          key (DRF would otherwise treat ``None`` as a clear).
      properties:
        status:
          $ref: '#/components/schemas/FeedbackStatusEnum'
        resolution_note:
          type: string
          default: ''
      required:
        - status
    PatientInboxDetail:
      type: object
      description: |-
        Full read shape for the Patient Inbox detail endpoint.

        Layered on top of :class:`NewPatientDraftDetailSerializer` -- the
        same JSON buckets, signature data URL, lifecycle pointers -- plus
        the inlined dedup-candidate list so a reviewer who lands directly
        on a draft URL doesn't have to round-trip back to the list to see
        the warning.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        practice:
          type: string
          format: uuid
          readOnly: true
          description: Practice the draft was submitted to.
        submitted_via:
          allOf:
            - $ref: '#/components/schemas/SubmittedViaDb2Enum'
          readOnly: true
          description: |-
            Source channel for the submission.

            * `kiosk` - Kiosk
            * `portal` - Patient Portal
            * `staff_form` - Staff-entered form
        submitted_at:
          type: string
          format: date-time
          readOnly: true
          description: When the submission was received by the API.
        tablet:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: >-
            Tablet (kiosk) that submitted this draft; null for portal /
            staff-form drafts.
        personal_info:
          readOnly: true
          description: Patient-supplied demographics; mirrors Patient model field names.
        insurance_info:
          readOnly: true
          description: Insurance demographics; photo refs point at apps.doccenter rows.
        medical_history:
          readOnly: true
          description: Patient-supplied medical history; mirrors clinical models' shape.
        signature_b64:
          type: string
          nullable: true
          readOnly: true
          description: >-
            Consent capture as a ``data:image/png;base64,<...>`` data URL, or
            ``null`` if the draft has no signature on file.
        status:
          allOf:
            - $ref: '#/components/schemas/Status53dEnum'
          readOnly: true
          description: |-
            Lifecycle state.

            * `pending` - Pending review
            * `ready_for_review` - Ready for review
            * `approved` - Approved -- new patient created
            * `converted` - Converted to new patient
            * `merged` - Merged into existing patient
            * `rejected` - Rejected
            * `dismissed` - Dismissed
        reviewed_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff user who took the approve / merge / reject action.
        reviewed_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When the draft was approved / merged / rejected.
        merged_into_patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: 'When status=merged: the existing patient that absorbed this draft.'
        converted_to_patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: 'When status=converted: the new patient created from this draft.'
        approved_patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: 'When status=approved: the new patient created from this draft.'
        rejection_reason:
          type: string
          readOnly: true
          description: Free-text reason for rejection; empty for non-rejected drafts.
        dedup_candidates:
          type: string
          readOnly: true
          description: >-
            Same dedup-candidate list as the list endpoint, embedded on the
            detail row.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - approved_patient
        - converted_to_patient
        - created
        - dedup_candidates
        - id
        - insurance_info
        - medical_history
        - merged_into_patient
        - modified
        - personal_info
        - practice
        - rejection_reason
        - reviewed_at
        - reviewed_by
        - signature_b64
        - status
        - submitted_at
        - submitted_via
        - tablet
    PatientInboxDismissRequest:
      type: object
      description: |-
        Optional-reason body for the dismiss action.

        Unlike :class:`NewPatientDraftRejectSerializer` -- which requires
        a reason -- dismiss is a one-click "this isn't a real patient"
        surface.  Staff who do supply a reason ("kid playing with kiosk")
        get it persisted on the row, but a blank submission is also
        accepted and lands the draft in ``dismissed`` without a note.
      properties:
        reason:
          type: string
          default: ''
          description: Optional free-text reason; persisted on the draft for audit.
          maxLength: 255
    PatientInboxListItem:
      type: object
      description: |-
        Compact read shape for the Patient Inbox list endpoint.

        Carries enough demographics to render the triage table, the
        lifecycle pointers (so the UI can render a "converted to <patient>"
        badge on terminal rows), and the dedup-candidate hint list so the
        reviewer can spot dupes before clicking convert.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        submitted_via:
          type: string
          readOnly: true
        submitted_at:
          type: string
          format: date-time
          readOnly: true
          description: When the submission was received by the API.
        status:
          type: string
          readOnly: true
        first_name:
          type: string
          readOnly: true
          description: Plucked from personal_info; PHI.
        last_name:
          type: string
          readOnly: true
          description: Plucked from personal_info; PHI.
        date_of_birth:
          type: string
          readOnly: true
          description: Plucked from personal_info; PHI.
        converted_to_patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: 'When status=converted: the new patient created from this draft.'
        merged_into_patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: 'When status=merged: the existing patient that absorbed this draft.'
        dedup_candidates:
          type: string
          readOnly: true
          description: >-
            Up to eight likely-duplicate patients at the same practice, ordered
            by confidence descending.  Empty when the draft has no usable
            features (e.g. a partial submission).
      required:
        - converted_to_patient
        - date_of_birth
        - dedup_candidates
        - first_name
        - id
        - last_name
        - merged_into_patient
        - status
        - submitted_at
        - submitted_via
    PatientInboxMergeRequest:
      type: object
      description: |-
        Body for the Patient Inbox merge action.

        Mirrors :class:`NewPatientDraftMergeSerializer` but uses the
        ``patient_id`` field name the umbrella issue spec calls out so
        the staff web's existing patient-picker can submit the value
        verbatim.  The view validates that the target belongs to the
        calling practice; the serializer just validates the shape.
      properties:
        patient_id:
          type: string
          format: uuid
          description: UUID of the existing patient that should absorb this draft.
      required:
        - patient_id
    PatientInsurance:
      type: object
      description: >-
        Read-only serializer for patient insurance enrollments with nested
        benefits.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        custom_data:
          readOnly: true
        benefits:
          type: array
          items:
            $ref: '#/components/schemas/InsuranceBenefit'
          readOnly: true
        carrier_detail:
          allOf:
            - $ref: '#/components/schemas/InsuranceCarrier'
          readOnly: true
        plan_detail:
          allOf:
            - $ref: '#/components/schemas/InsurancePlanList'
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        carrier_name:
          type: string
          readOnly: true
          maxLength: 255
        group_number:
          type: string
          readOnly: true
          maxLength: 100
        subscriber_id:
          type: string
          readOnly: true
          maxLength: 100
        subscriber_name:
          type: string
          readOnly: true
          description: Policy holder name if different from patient
          maxLength: 255
        ordinal:
          type: integer
          maximum: 32767
          minimum: 0
          readOnly: true
          default: 1
          description: 1=primary, 2=secondary
        relationship_to_subscriber:
          readOnly: true
          oneOf:
            - $ref: '#/components/schemas/RelationshipToSubscriberEnum'
            - $ref: '#/components/schemas/BlankEnum'
        effective_date:
          type: string
          format: date
          readOnly: true
          nullable: true
        termination_date:
          type: string
          format: date
          readOnly: true
          nullable: true
        created_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        modified_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        practice:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        insurance_plan:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        carrier:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        fee_schedule:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Fee schedule used for benefit estimation under this plan
      required:
        - benefits
        - carrier
        - carrier_detail
        - carrier_name
        - created
        - created_by
        - custom_data
        - effective_date
        - fee_schedule
        - group_number
        - id
        - insurance_plan
        - modified
        - modified_by
        - ordinal
        - patient
        - plan_detail
        - practice
        - relationship_to_subscriber
        - subscriber_id
        - subscriber_name
        - termination_date
    PatientInsuranceBulkWrite:
      type: object
      description: >-
        Write serializer for practice-wide bulk patient-insurance import.


        Differs from :class:`PatientInsuranceWriteSerializer` in two ways:


        1. ``patient`` is a writable required field (the practice-wide endpoint
           doesn't take ``patient_id`` from the URL, so each record must carry
           its own FK).
        2. If a record is POSTed with a ``carrier_name`` string but no
        ``carrier``
           FK, the serializer resolves-or-creates an :class:`InsuranceCarrier`
           row in the current practice scoped by name (case-insensitive) and
           links the enrollment to it.  This avoids the previous behavior
           where bulk imports created dangling enrollments with no carrier FK,
           and matches the semantics the sync tools expect: each PMS-level
           carrier string becomes exactly one ``InsuranceCarrier`` row.

        GH-711: the directly-supplied ``patient`` / ``insurance_plan`` /

        ``carrier`` / ``fee_schedule`` FKs are checked against the URL practice.

        The carrier-by-name resolver already scopes its lookup to the URL

        practice; only explicit UUID FK refs need validator coverage.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
        insurance_plan:
          type: string
          format: uuid
          nullable: true
        carrier_name:
          type: string
          maxLength: 255
        carrier:
          type: string
          format: uuid
          nullable: true
        fee_schedule:
          type: string
          format: uuid
          nullable: true
          description: Fee schedule used for benefit estimation under this plan
        group_number:
          type: string
          maxLength: 100
        subscriber_id:
          type: string
          maxLength: 100
        subscriber_name:
          type: string
          description: Policy holder name if different from patient
          maxLength: 255
        ordinal:
          type: integer
          maximum: 32767
          minimum: 0
          description: 1=primary, 2=secondary
        relationship_to_subscriber:
          oneOf:
            - $ref: '#/components/schemas/RelationshipToSubscriberEnum'
            - $ref: '#/components/schemas/BlankEnum'
        effective_date:
          type: string
          format: date
          nullable: true
        termination_date:
          type: string
          format: date
          nullable: true
        custom_data: {}
      required:
        - carrier_name
        - id
        - patient
    PatientInsuranceBulkWriteRequest:
      type: object
      description: >-
        Write serializer for practice-wide bulk patient-insurance import.


        Differs from :class:`PatientInsuranceWriteSerializer` in two ways:


        1. ``patient`` is a writable required field (the practice-wide endpoint
           doesn't take ``patient_id`` from the URL, so each record must carry
           its own FK).
        2. If a record is POSTed with a ``carrier_name`` string but no
        ``carrier``
           FK, the serializer resolves-or-creates an :class:`InsuranceCarrier`
           row in the current practice scoped by name (case-insensitive) and
           links the enrollment to it.  This avoids the previous behavior
           where bulk imports created dangling enrollments with no carrier FK,
           and matches the semantics the sync tools expect: each PMS-level
           carrier string becomes exactly one ``InsuranceCarrier`` row.

        GH-711: the directly-supplied ``patient`` / ``insurance_plan`` /

        ``carrier`` / ``fee_schedule`` FKs are checked against the URL practice.

        The carrier-by-name resolver already scopes its lookup to the URL

        practice; only explicit UUID FK refs need validator coverage.
      properties:
        patient:
          type: string
          format: uuid
        insurance_plan:
          type: string
          format: uuid
          nullable: true
        carrier_name:
          type: string
          minLength: 1
          maxLength: 255
        carrier:
          type: string
          format: uuid
          nullable: true
        fee_schedule:
          type: string
          format: uuid
          nullable: true
          description: Fee schedule used for benefit estimation under this plan
        group_number:
          type: string
          maxLength: 100
        subscriber_id:
          type: string
          maxLength: 100
        subscriber_name:
          type: string
          description: Policy holder name if different from patient
          maxLength: 255
        ordinal:
          type: integer
          maximum: 32767
          minimum: 0
          description: 1=primary, 2=secondary
        relationship_to_subscriber:
          oneOf:
            - $ref: '#/components/schemas/RelationshipToSubscriberEnum'
            - $ref: '#/components/schemas/BlankEnum'
        effective_date:
          type: string
          format: date
          nullable: true
        termination_date:
          type: string
          format: date
          nullable: true
        custom_data: {}
      required:
        - carrier_name
        - patient
    PatientInsuranceWrite:
      type: object
      description: Write serializer for patient insurance enrollments.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        insurance_plan:
          type: string
          format: uuid
          nullable: true
        carrier_name:
          type: string
          maxLength: 255
        carrier:
          type: string
          format: uuid
          nullable: true
        fee_schedule:
          type: string
          format: uuid
          nullable: true
          description: Fee schedule used for benefit estimation under this plan
        group_number:
          type: string
          maxLength: 100
        subscriber_id:
          type: string
          maxLength: 100
        subscriber_name:
          type: string
          description: Policy holder name if different from patient
          maxLength: 255
        ordinal:
          type: integer
          maximum: 32767
          minimum: 0
          description: 1=primary, 2=secondary
        relationship_to_subscriber:
          oneOf:
            - $ref: '#/components/schemas/RelationshipToSubscriberEnum'
            - $ref: '#/components/schemas/BlankEnum'
        effective_date:
          type: string
          format: date
          nullable: true
        termination_date:
          type: string
          format: date
          nullable: true
        custom_data: {}
      required:
        - carrier_name
        - id
    PatientInsuranceWriteRequest:
      type: object
      description: Write serializer for patient insurance enrollments.
      properties:
        insurance_plan:
          type: string
          format: uuid
          nullable: true
        carrier_name:
          type: string
          minLength: 1
          maxLength: 255
        carrier:
          type: string
          format: uuid
          nullable: true
        fee_schedule:
          type: string
          format: uuid
          nullable: true
          description: Fee schedule used for benefit estimation under this plan
        group_number:
          type: string
          maxLength: 100
        subscriber_id:
          type: string
          maxLength: 100
        subscriber_name:
          type: string
          description: Policy holder name if different from patient
          maxLength: 255
        ordinal:
          type: integer
          maximum: 32767
          minimum: 0
          description: 1=primary, 2=secondary
        relationship_to_subscriber:
          oneOf:
            - $ref: '#/components/schemas/RelationshipToSubscriberEnum'
            - $ref: '#/components/schemas/BlankEnum'
        effective_date:
          type: string
          format: date
          nullable: true
        termination_date:
          type: string
          format: date
          nullable: true
        custom_data: {}
      required:
        - carrier_name
    PatientMedication:
      type: object
      description: |-
        Full CRUD serializer for patient medications.

        The ``patient`` field is read-only because it is set from the URL path.
        GH-711: bulk-injected ``patient`` is practice-validated.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        medication_name:
          type: string
          maxLength: 255
        date_start:
          type: string
          format: date
          nullable: true
        date_stop:
          type: string
          format: date
          nullable: true
        note:
          type: string
        rx_cui:
          type: string
          description: RxNorm concept unique identifier
          maxLength: 20
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - created
        - id
        - medication_name
        - modified
        - patient
    PatientMedicationRequest:
      type: object
      description: |-
        Full CRUD serializer for patient medications.

        The ``patient`` field is read-only because it is set from the URL path.
        GH-711: bulk-injected ``patient`` is practice-validated.
      properties:
        medication_name:
          type: string
          minLength: 1
          maxLength: 255
        date_start:
          type: string
          format: date
          nullable: true
        date_stop:
          type: string
          format: date
          nullable: true
        note:
          type: string
        rx_cui:
          type: string
          description: RxNorm concept unique identifier
          maxLength: 20
        custom_data: {}
      required:
        - medication_name
    PatientRequest:
      type: object
      description: >-
        GH-711: ``primary_provider`` / ``secondary_provider`` FKs are
        practice-validated.
      properties:
        first_name:
          type: string
          minLength: 1
          maxLength: 100
        last_name:
          type: string
          minLength: 1
          maxLength: 100
        preferred_name:
          type: string
          maxLength: 100
        email:
          type: string
          format: email
          maxLength: 254
        date_of_birth:
          type: string
          format: date
          nullable: true
        gender:
          $ref: '#/components/schemas/GenderEnum'
        status:
          $ref: '#/components/schemas/PatientStatusEnum'
        phone_home:
          type: string
          nullable: true
        phone_work:
          type: string
          nullable: true
        phone_mobile:
          type: string
          nullable: true
        address:
          type: string
          maxLength: 255
        address2:
          type: string
          maxLength: 255
        city:
          type: string
          maxLength: 100
        state:
          type: string
          maxLength: 50
        zip_code:
          type: string
          pattern: ^\d{5}(-\d{4})?$
          maxLength: 10
        primary_provider:
          type: string
          format: uuid
          nullable: true
        secondary_provider:
          type: string
          format: uuid
          nullable: true
        balance_current:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: >-
            Current account balance, denormalized from the ledger for fast list
            rendering.
        last_recall_date:
          type: string
          format: date
          nullable: true
          description: Most recent recall appointment date from the source PMS
        next_recall_date:
          type: string
          format: date
          nullable: true
          description: Next scheduled recall appointment date from the source PMS
        prefers_email:
          type: boolean
          description: Patient opts in to email communications (per source PMS)
        prefers_sms:
          type: boolean
          description: Patient opts in to SMS/text communications (per source PMS)
        hipaa_consent_signed:
          type: boolean
          description: HIPAA treatment consent signed (per source PMS)
        hipaa_consent_date:
          type: string
          format: date
          nullable: true
          description: Date HIPAA treatment consent was signed
        hipaa_authorization_signed:
          type: boolean
          description: HIPAA authorization (release of info) signed (per source PMS)
        hipaa_authorization_date:
          type: string
          format: date
          nullable: true
          description: Date HIPAA authorization was signed
        hipaa_privacy_practices_signed:
          type: boolean
          description: HIPAA notice of privacy practices acknowledged (per source PMS)
        hipaa_privacy_practices_date:
          type: string
          format: date
          nullable: true
          description: Date HIPAA notice of privacy practices was acknowledged
        custom_data: {}
      required:
        - first_name
        - last_name
    PatientStatusEnum:
      enum:
        - active
        - inactive
        - archived
        - deceased
        - prospective
      type: string
      description: |-
        * `active` - Active
        * `inactive` - Inactive
        * `archived` - Archived
        * `deceased` - Deceased
        * `prospective` - Prospective
    PatientSubmissionDetail:
      type: object
      description: >-
        Full detail representation including submitted_data and
        rejection_reason.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        practice:
          type: string
          format: uuid
          readOnly: true
        invite_token:
          type: string
          readOnly: true
          description: One-time use token included in the invite URL.
        invite_expires_at:
          type: string
          format: date-time
          readOnly: true
          description: Invite becomes invalid after this timestamp.
        status:
          allOf:
            - $ref: '#/components/schemas/PatientSubmissionStatusEnum'
          readOnly: true
        submitted_data:
          readOnly: true
          description: >-
            Demographics, contact, insurance, and medical history submitted by
            the patient.
        submitted_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: Timestamp when the patient submitted data (marks token as used).
        reviewed_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: Timestamp when the submission was approved or rejected.
        reviewed_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        rejection_reason:
          type: string
          readOnly: true
          description: Staff-provided reason when a submission is rejected.
        patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Set when the submission is approved and a Patient record is created.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - id
        - invite_expires_at
        - invite_token
        - patient
        - practice
        - rejection_reason
        - reviewed_at
        - reviewed_by
        - status
        - submitted_at
        - submitted_data
    PatientSubmissionInviteResponse:
      type: object
      description: Response after creating an invite.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        invite_token:
          type: string
          readOnly: true
          description: One-time use token included in the invite URL.
        invite_expires_at:
          type: string
          format: date-time
          readOnly: true
          description: Invite becomes invalid after this timestamp.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - id
        - invite_expires_at
        - invite_token
    PatientSubmissionList:
      type: object
      description: Read-only list representation of a patient submission.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        practice:
          type: string
          format: uuid
          readOnly: true
        invite_token:
          type: string
          readOnly: true
          description: One-time use token included in the invite URL.
        invite_expires_at:
          type: string
          format: date-time
          readOnly: true
          description: Invite becomes invalid after this timestamp.
        status:
          allOf:
            - $ref: '#/components/schemas/PatientSubmissionStatusEnum'
          readOnly: true
        submitted_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: Timestamp when the patient submitted data (marks token as used).
        reviewed_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: Timestamp when the submission was approved or rejected.
        reviewed_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Set when the submission is approved and a Patient record is created.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - id
        - invite_expires_at
        - invite_token
        - patient
        - practice
        - reviewed_at
        - reviewed_by
        - status
        - submitted_at
    PatientSubmissionRejectRequest:
      type: object
      description: Payload for rejecting a submission.
      properties:
        rejection_reason:
          type: string
          minLength: 1
          description: Staff-provided reason for the rejection.
      required:
        - rejection_reason
    PatientSubmissionStatusEnum:
      enum:
        - pending
        - approved
        - rejected
      type: string
      description: |-
        * `pending` - Pending Review
        * `approved` - Approved
        * `rejected` - Rejected
    PatientSubmissionSubmitRequest:
      type: object
      description: |-
        Payload the prospective patient sends when submitting their data.

        ``submitted_data`` is an open JSON object containing demographics,
        contact information, insurance details, and medical history.
      properties:
        submitted_data:
          description: Patient intake data (demographics, etc.).
      required:
        - submitted_data
    PayerMixPracticeRow:
      type: object
      description: Per-practice payer-mix breakdown.
      properties:
        practice_id:
          type: string
        practice_name:
          type: string
        patient_total:
          type: integer
        carriers:
          type: array
          items:
            $ref: '#/components/schemas/_CarrierMixRow'
      required:
        - carriers
        - patient_total
        - practice_id
        - practice_name
    PayerMixResponse:
      type: object
      description: Response for ``/reports/payer-mix/``.
      properties:
        practices:
          type: array
          items:
            $ref: '#/components/schemas/PayerMixPracticeRow'
      required:
        - practices
    PaymentIntent:
      type: object
      description: Read-only representation of a payment intent.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        stripe_payment_intent_id:
          type: string
          readOnly: true
        amount_cents:
          type: integer
          readOnly: true
          description: Amount in cents (USD).
        amount_dollars:
          type: string
          readOnly: true
        status:
          allOf:
            - $ref: '#/components/schemas/StripeStatusEnum'
          readOnly: true
        description:
          type: string
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - amount_cents
        - amount_dollars
        - created
        - description
        - id
        - status
        - stripe_payment_intent_id
    PaymentMethodCard:
      type: object
      description: A single saved card in the portal's sanitized shape.
      properties:
        id:
          type: string
          description: Stripe PaymentMethod id (``pm_...``).
        brand:
          type: string
          description: 'Card brand: visa / mastercard / amex / discover / ...'
        last4:
          type: string
          description: Last 4 digits of the card number.
        exp_month:
          type: integer
          nullable: true
        exp_year:
          type: integer
          nullable: true
        created:
          type: integer
          nullable: true
          description: >-
            Stripe creation timestamp (Unix epoch seconds).  Used for
            sort-by-most-recent.
      required:
        - brand
        - created
        - exp_month
        - exp_year
        - id
        - last4
    PaymentMethodListResponse:
      type: object
      description: Envelope for the saved-cards list endpoint.
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/PaymentMethodCard'
      required:
        - results
    PaymentPlan:
      type: object
      description: Read-only serializer for payment plans with nested charges.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        custom_data:
          readOnly: true
        charges:
          type: array
          items:
            $ref: '#/components/schemas/PaymentPlanCharge'
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        plan_date:
          type: string
          format: date
          readOnly: true
          nullable: true
        apr:
          type: string
          format: decimal
          pattern: ^-?\d{0,3}(?:\.\d{0,2})?$
          readOnly: true
        completed_amt:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        is_closed:
          type: boolean
          readOnly: true
        created_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        modified_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        practice:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        guarantor:
          type: string
          format: uuid
          readOnly: true
          nullable: true
      required:
        - apr
        - charges
        - completed_amt
        - created
        - created_by
        - custom_data
        - guarantor
        - id
        - is_closed
        - modified
        - modified_by
        - patient
        - plan_date
        - practice
    PaymentPlanCharge:
      type: object
      description: Read-only serializer for individual payment plan charges.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        custom_data:
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        charge_date:
          type: string
          format: date
          readOnly: true
          nullable: true
        principal:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        interest:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        note:
          type: string
          readOnly: true
        created_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        modified_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        practice:
          type: string
          format: uuid
          readOnly: true
        payment_plan:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
      required:
        - charge_date
        - created
        - created_by
        - custom_data
        - id
        - interest
        - modified
        - modified_by
        - note
        - patient
        - payment_plan
        - practice
        - principal
    PaymentPlanChargeWrite:
      type: object
      description: >-
        Write serializer for individual charges within a payment plan.


        Used as a nested serializer inside ``PaymentPlanWriteSerializer``.

        Practice, patient, and payment_plan are set by the parent
        view/serializer.
      properties:
        charge_date:
          type: string
          format: date
          nullable: true
        principal:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        interest:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        note:
          type: string
        custom_data: {}
    PaymentPlanChargeWriteRequest:
      type: object
      description: >-
        Write serializer for individual charges within a payment plan.


        Used as a nested serializer inside ``PaymentPlanWriteSerializer``.

        Practice, patient, and payment_plan are set by the parent
        view/serializer.
      properties:
        charge_date:
          type: string
          format: date
          nullable: true
        principal:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        interest:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        note:
          type: string
        custom_data: {}
    PaymentPlanWrite:
      type: object
      description: >-
        Write serializer for creating a payment plan with nested charges.


        Practice and patient are set from the URL path by the view.

        Accepts an optional ``charges`` array to create charges in a single
        request.


        GH-711: ``guarantor`` (Patient FK) and the injected ``patient`` FK on

        the bulk path are validated against the URL practice.
      properties:
        guarantor:
          type: string
          format: uuid
          nullable: true
        plan_date:
          type: string
          format: date
          nullable: true
        apr:
          type: string
          format: decimal
          pattern: ^-?\d{0,3}(?:\.\d{0,2})?$
        completed_amt:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        is_closed:
          type: boolean
        charges:
          type: array
          items:
            $ref: '#/components/schemas/PaymentPlanChargeWrite'
        custom_data: {}
    PaymentPlanWriteRequest:
      type: object
      description: >-
        Write serializer for creating a payment plan with nested charges.


        Practice and patient are set from the URL path by the view.

        Accepts an optional ``charges`` array to create charges in a single
        request.


        GH-711: ``guarantor`` (Patient FK) and the injected ``patient`` FK on

        the bulk path are validated against the URL practice.
      properties:
        guarantor:
          type: string
          format: uuid
          nullable: true
        plan_date:
          type: string
          format: date
          nullable: true
        apr:
          type: string
          format: decimal
          pattern: ^-?\d{0,3}(?:\.\d{0,2})?$
        completed_amt:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        is_closed:
          type: boolean
        charges:
          type: array
          items:
            $ref: '#/components/schemas/PaymentPlanChargeWriteRequest'
        custom_data: {}
    PaymentRecord:
      type: object
      description: >-
        Read-only serializer for payment records with nested splits.


        In addition to every concrete ``PaymentRecord`` column, the list payload

        exposes pre-joined display fields -- ``patient_name`` /
        ``provider_name``

        (``{last}, {first}`` format, matching :class:`AppointmentSerializer`),
        the

        raw ``patient_first_name`` / ``patient_last_name`` pieces, and the

        computed ``pay_method`` bucket
        (``cash``/``check``/``card``/``insurance``/

        ``other``) -- so report rows can render without a second round-trip.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        custom_data:
          readOnly: true
        splits:
          type: array
          items:
            $ref: '#/components/schemas/PaymentSplit'
          readOnly: true
        patient_name:
          type: string
          readOnly: true
        patient_first_name:
          type: string
          readOnly: true
        patient_last_name:
          type: string
          readOnly: true
        provider_name:
          type: string
          readOnly: true
        pay_method:
          type: string
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        pay_date:
          type: string
          format: date
          readOnly: true
          nullable: true
        pay_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        pay_type:
          type: string
          readOnly: true
          maxLength: 100
        pay_note:
          type: string
          readOnly: true
        stripe_payment_intent_id:
          type: string
          readOnly: true
          description: >-
            Stripe PaymentIntent id (``pi_...``) when this record was charged
            via Stripe.
          maxLength: 255
        stripe_status:
          readOnly: true
          description: >-
            Stripe charge lifecycle.  Empty for non-Stripe records (cash, check,
            PMS import, etc.).


            * `PENDING` - Pending

            * `SUCCEEDED` - Succeeded

            * `FAILED` - Failed

            * `REFUNDED` - Refunded
          oneOf:
            - $ref: '#/components/schemas/StripeStatusEnum'
            - $ref: '#/components/schemas/BlankEnum'
        created_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        modified_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        practice:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        provider:
          type: string
          format: uuid
          readOnly: true
          nullable: true
      required:
        - created
        - created_by
        - custom_data
        - id
        - modified
        - modified_by
        - patient
        - patient_first_name
        - patient_last_name
        - patient_name
        - pay_amount
        - pay_date
        - pay_method
        - pay_note
        - pay_type
        - practice
        - provider
        - provider_name
        - splits
        - stripe_payment_intent_id
        - stripe_status
    PaymentRecordWrite:
      type: object
      description: |-
        Create a payment record with optional nested splits.

        Practice and patient are set by the view.
      properties:
        pay_date:
          type: string
          format: date
          nullable: true
        pay_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        pay_type:
          type: string
          maxLength: 100
        pay_note:
          type: string
        provider:
          type: string
          format: uuid
          nullable: true
        custom_data: {}
        splits:
          type: array
          items:
            $ref: '#/components/schemas/PaymentSplitWrite'
    PaymentRecordWriteRequest:
      type: object
      description: |-
        Create a payment record with optional nested splits.

        Practice and patient are set by the view.
      properties:
        pay_date:
          type: string
          format: date
          nullable: true
        pay_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        pay_type:
          type: string
          maxLength: 100
        pay_note:
          type: string
        provider:
          type: string
          format: uuid
          nullable: true
        custom_data: {}
        splits:
          type: array
          items:
            $ref: '#/components/schemas/PaymentSplitWriteRequest'
    PaymentSplit:
      type: object
      description: |-
        Read-only serializer for individual payment split lines.

        Both the legacy string ``proc_code`` (authoritative for PMS imports
        with no canonical ProcedureCode match) and the nested
        ``procedure_code`` FK object (populated when the resolver found a
        matching :class:`clinical.ProcedureCode`) are exposed so clients can
        render either without a second round-trip.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        custom_data:
          readOnly: true
        procedure_code:
          allOf:
            - $ref: '#/components/schemas/ProcedureCodeNested'
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        split_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        proc_code:
          type: string
          readOnly: true
          maxLength: 20
        procedure_date:
          type: string
          format: date
          readOnly: true
          nullable: true
        note:
          type: string
          readOnly: true
        created_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        modified_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        practice:
          type: string
          format: uuid
          readOnly: true
        payment:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
      required:
        - created
        - created_by
        - custom_data
        - id
        - modified
        - modified_by
        - note
        - patient
        - payment
        - practice
        - proc_code
        - procedure_code
        - procedure_date
        - split_amount
    PaymentSplitWrite:
      type: object
      description: |-
        Write serializer for individual payment split lines.

        Used as a nested serializer inside :class:`PaymentRecordWriteSerializer`
        on the patient-scoped POST path; ``payment``, ``practice``, and
        ``patient`` are set by the parent.  For the standalone bulk-import
        path use :class:`PaymentSplitBulkWriteSerializer`.
      properties:
        split_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        proc_code:
          type: string
          maxLength: 20
        procedure_code:
          type: string
          format: uuid
          nullable: true
        procedure_date:
          type: string
          format: date
          nullable: true
        note:
          type: string
        custom_data: {}
    PaymentSplitWriteRequest:
      type: object
      description: |-
        Write serializer for individual payment split lines.

        Used as a nested serializer inside :class:`PaymentRecordWriteSerializer`
        on the patient-scoped POST path; ``payment``, ``practice``, and
        ``patient`` are set by the parent.  For the standalone bulk-import
        path use :class:`PaymentSplitBulkWriteSerializer`.
      properties:
        split_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        proc_code:
          type: string
          maxLength: 20
        procedure_code:
          type: string
          format: uuid
          nullable: true
        procedure_date:
          type: string
          format: date
          nullable: true
        note:
          type: string
        custom_data: {}
    PerioExam:
      type: object
      description: >-
        Read serializer for perio exams with nested readings and denormalized
        provider name.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        provider:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        provider_name:
          type: string
          nullable: true
          readOnly: true
        appointment:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        exam_date:
          type: string
          format: date-time
          readOnly: true
        is_completed:
          type: boolean
          readOnly: true
          description: Completed exams cannot be deleted
        stage:
          allOf:
            - $ref: '#/components/schemas/StageEnum'
          readOnly: true
        grade:
          allOf:
            - $ref: '#/components/schemas/GradeEnum'
          readOnly: true
        note:
          type: string
          readOnly: true
        readings:
          type: array
          items:
            $ref: '#/components/schemas/PerioReading'
          readOnly: true
        reading_count:
          type: integer
          description: Return the number of tooth readings in this exam.
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - appointment
        - created
        - exam_date
        - grade
        - id
        - is_completed
        - modified
        - note
        - patient
        - provider
        - provider_name
        - reading_count
        - readings
        - stage
    PerioExamWrite:
      type: object
      description: |-
        Write serializer for creating and updating perio exams.

        The ``patient`` field is excluded because it is set from the URL path
        by the view's ``perform_create`` method.

        GH-711: ``patient`` / ``provider`` / ``appointment`` FKs are
        practice-validated.
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        appointment:
          type: string
          format: uuid
          nullable: true
        exam_date:
          type: string
          format: date-time
        is_completed:
          type: boolean
          description: Completed exams cannot be deleted
        stage:
          oneOf:
            - $ref: '#/components/schemas/StageEnum'
            - $ref: '#/components/schemas/BlankEnum'
        grade:
          oneOf:
            - $ref: '#/components/schemas/GradeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        note:
          type: string
        custom_data: {}
      required:
        - exam_date
    PerioExamWriteRequest:
      type: object
      description: |-
        Write serializer for creating and updating perio exams.

        The ``patient`` field is excluded because it is set from the URL path
        by the view's ``perform_create`` method.

        GH-711: ``patient`` / ``provider`` / ``appointment`` FKs are
        practice-validated.
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        appointment:
          type: string
          format: uuid
          nullable: true
        exam_date:
          type: string
          format: date-time
        is_completed:
          type: boolean
          description: Completed exams cannot be deleted
        stage:
          oneOf:
            - $ref: '#/components/schemas/StageEnum'
            - $ref: '#/components/schemas/BlankEnum'
        grade:
          oneOf:
            - $ref: '#/components/schemas/GradeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        note:
          type: string
        custom_data: {}
      required:
        - exam_date
    PerioReading:
      type: object
      description: >-
        Full CRUD serializer for individual perio readings (6-point probing data
        per tooth).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        tooth_num:
          type: string
          description: 'Universal numbering: 1-32'
          maxLength: 5
        probe_1:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Mesio-facial depth (mm)
        probe_2:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Mid-facial depth (mm)
        probe_3:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Disto-facial depth (mm)
        probe_4:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Mesio-lingual depth (mm)
        probe_5:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Mid-lingual depth (mm)
        probe_6:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Disto-lingual depth (mm)
        recession_1:
          type: integer
          maximum: 32767
          minimum: -32768
          nullable: true
        recession_2:
          type: integer
          maximum: 32767
          minimum: -32768
          nullable: true
        recession_3:
          type: integer
          maximum: 32767
          minimum: -32768
          nullable: true
        recession_4:
          type: integer
          maximum: 32767
          minimum: -32768
          nullable: true
        recession_5:
          type: integer
          maximum: 32767
          minimum: -32768
          nullable: true
        recession_6:
          type: integer
          maximum: 32767
          minimum: -32768
          nullable: true
        bleed_1:
          type: boolean
        bleed_2:
          type: boolean
        bleed_3:
          type: boolean
        bleed_4:
          type: boolean
        bleed_5:
          type: boolean
        bleed_6:
          type: boolean
        suppuration_1:
          type: boolean
        suppuration_2:
          type: boolean
        suppuration_3:
          type: boolean
        suppuration_4:
          type: boolean
        suppuration_5:
          type: boolean
        suppuration_6:
          type: boolean
        plaque_1:
          type: boolean
        plaque_2:
          type: boolean
        plaque_3:
          type: boolean
        plaque_4:
          type: boolean
        plaque_5:
          type: boolean
        plaque_6:
          type: boolean
        furcation:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: 0-3 furcation class
        mobility:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: 0-3 mobility grade
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - created
        - id
        - modified
        - tooth_num
    PermissionEnum:
      enum:
        - patient.can_share
        - reports.all
        - reports.production
        - reports.ar
        - reports.patients
        - reports.operations
        - reports.compliance
      type: string
      description: |-
        * `patient.can_share` - Patient: Can Share
        * `reports.all` - Reports: All Categories
        * `reports.production` - Reports: Production
        * `reports.ar` - Reports: A/R
        * `reports.patients` - Reports: Patients
        * `reports.operations` - Reports: Operations
        * `reports.compliance` - Reports: Compliance
    PingResponse:
      type: object
      description: Health-check response from PingView.
      properties:
        status:
          type: string
      required:
        - status
    Plan:
      type: object
      description: |-
        Read-only public representation of a subscription tier.

        Plans are admin-managed -- the customer cannot create or edit them via
        the API, so this serializer is intentionally read-only and excludes the
        internal ``is_active`` flag (used to retire tiers without deleting
        historical rows).  The marketing site reads this endpoint to render
        pricing tiles, so the field set is the customer-facing contract.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        slug:
          type: string
          readOnly: true
          description: >-
            Stable identifier (free, standard, advanced, escape). Used in code
            and Stripe.
        display_name:
          type: string
          readOnly: true
          description: Customer-facing name shown on pricing tiles.
        description:
          type: string
          readOnly: true
          description: Marketing copy / one-liner shown next to the tile.
        monthly_price_cents:
          type: integer
          readOnly: true
          description: Monthly recurring price in USD cents (e.g. 15000 = $150.00/mo).
        max_practices:
          type: integer
          readOnly: true
          description: Maximum number of Practice rows the owning Client may have.
        max_users_per_practice:
          type: integer
          readOnly: true
          description: Maximum active PracticeMembership rows per Practice on this plan.
        includes_fax:
          type: boolean
          readOnly: true
          description: Plan grants access to outbound fax features.
        includes_sms:
          type: boolean
          readOnly: true
          description: Plan grants access to outbound SMS features.
        api_rate_limit_per_minute:
          type: integer
          readOnly: true
          description: Per-token API request ceiling, requests/minute.
      required:
        - api_rate_limit_per_minute
        - description
        - display_name
        - id
        - includes_fax
        - includes_sms
        - max_practices
        - max_users_per_practice
        - monthly_price_cents
        - slug
    PlanLimitError:
      type: object
      description: >-
        OpenAPI shape for the structured 402 returned when a Plan cap rejects a
        request.


        Mirrors the body :func:`apps.common.exception_handler.exception_handler`

        renders for :class:`apps.practices.plan_limits.PlanLimitExceeded`.

        Declared as a serializer so the drf-spectacular schema can advertise

        the exact field shape to client apps; it is never used to *parse*

        request bodies (the exception handler synthesizes the response

        directly).
      properties:
        detail:
          type: string
          description: >-
            Human-readable explanation, e.g. 'Your current plan (Free) supports
            up to 2 users per practice. Upgrade to add more.'
        code:
          type: string
          description: >-
            Stable machine token; always ``plan_limit_exceeded`` for this
            response.
        limit:
          type: string
          description: >-
            The Plan field that capped the action.  One of ``max_practices``,
            ``max_users_per_practice``, ``includes_fax``, ``includes_sms``.
        upgrade_url:
          type: string
          description: >-
            Frontend route the UI should deep-link the user to so they can
            resolve the rejection.
      required:
        - code
        - detail
        - limit
        - upgrade_url
    PlanTypeEnum:
      enum:
        - ppo
        - hmo
        - dhmo
        - indemnity
        - capitation
        - other
      type: string
      description: |-
        * `ppo` - PPO
        * `hmo` - HMO
        * `dhmo` - DHMO
        * `indemnity` - Indemnity
        * `capitation` - Capitation
        * `other` - Other
    PollCreate:
      type: object
      description: Accepts question, settings, and nested options for poll creation.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        question:
          type: string
          maxLength: 500
        is_anonymous:
          type: boolean
          description: When true, results show vote counts but not voter identity.
        expires_at:
          type: string
          format: date-time
          description: UTC timestamp when the poll closes for voting.
        options:
          type: array
          items:
            $ref: '#/components/schemas/PollOptionWrite'
      required:
        - expires_at
        - id
        - options
        - question
    PollCreateRequest:
      type: object
      description: Accepts question, settings, and nested options for poll creation.
      properties:
        question:
          type: string
          minLength: 1
          maxLength: 500
        is_anonymous:
          type: boolean
          description: When true, results show vote counts but not voter identity.
        expires_at:
          type: string
          format: date-time
          description: UTC timestamp when the poll closes for voting.
        options:
          type: array
          items:
            $ref: '#/components/schemas/PollOptionWriteRequest'
      required:
        - expires_at
        - options
        - question
    PollDetail:
      type: object
      description: Full poll representation with options (no results until expired).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        question:
          type: string
          readOnly: true
        is_anonymous:
          type: boolean
          readOnly: true
          description: When true, results show vote counts but not voter identity.
        expires_at:
          type: string
          format: date-time
          readOnly: true
          description: UTC timestamp when the poll closes for voting.
        is_expired:
          type: boolean
          readOnly: true
        options:
          type: array
          items:
            $ref: '#/components/schemas/PollOption'
          readOnly: true
        has_voted:
          type: string
          readOnly: true
        my_vote_option_id:
          type: string
          readOnly: true
        created_by_name:
          type: string
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - created_by_name
        - expires_at
        - has_voted
        - id
        - is_anonymous
        - is_expired
        - my_vote_option_id
        - options
        - question
    PollList:
      type: object
      description: Compact representation for the poll list endpoint.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        question:
          type: string
          readOnly: true
        is_anonymous:
          type: boolean
          readOnly: true
          description: When true, results show vote counts but not voter identity.
        expires_at:
          type: string
          format: date-time
          readOnly: true
          description: UTC timestamp when the poll closes for voting.
        is_expired:
          type: boolean
          readOnly: true
        option_count:
          type: string
          readOnly: true
        has_voted:
          type: string
          readOnly: true
        created_by_name:
          type: string
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - created_by_name
        - expires_at
        - has_voted
        - id
        - is_anonymous
        - is_expired
        - option_count
        - question
    PollOption:
      type: object
      description: Read-only option representation for poll detail.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        text:
          type: string
          readOnly: true
        sort_order:
          type: integer
          readOnly: true
      required:
        - id
        - sort_order
        - text
    PollOptionWrite:
      type: object
      description: Inline option payload used when creating a poll.
      properties:
        text:
          type: string
          maxLength: 255
        sort_order:
          type: integer
          default: 0
      required:
        - text
    PollOptionWriteRequest:
      type: object
      description: Inline option payload used when creating a poll.
      properties:
        text:
          type: string
          minLength: 1
          maxLength: 255
        sort_order:
          type: integer
          default: 0
      required:
        - text
    PollResultOption:
      type: object
      description: 'Per-option result: count + optional voter names.'
      properties:
        id:
          type: string
          format: uuid
        text:
          type: string
        vote_count:
          type: integer
        voters:
          type: array
          items:
            type: string
      required:
        - id
        - text
        - vote_count
    PollResults:
      type: object
      description: Aggregated poll results returned after expiration.
      properties:
        poll_id:
          type: string
          format: uuid
        question:
          type: string
        is_anonymous:
          type: boolean
        total_votes:
          type: integer
        options:
          type: array
          items:
            $ref: '#/components/schemas/PollResultOption'
      required:
        - is_anonymous
        - options
        - poll_id
        - question
        - total_votes
    PortalAppointment:
      type: object
      description: |-
        Patient-portal-safe representation of a scheduled appointment.

        Deliberately a plain ``Serializer`` (not ``ModelSerializer``) so we
        have full control over what surfaces -- clinical notes, treatment
        plan IDs, dollars_scheduled, the long-tail status taxonomy, and any
        other staff-only field stay server-side.

        HIPAA: the ``procedure_description`` field is the patient-visible
        appointment summary populated by the source PMS (e.g. "Cleaning &
        Checkup").  Clinical notes (diagnoses, treatment plans, intra-op
        notes) live on different model fields and MUST NOT leak through this
        surface.  See ``apps.portal.views.PortalAppointmentListView``.
      properties:
        uid:
          type: string
          format: uuid
          readOnly: true
        start_time:
          type: string
          format: date-time
          readOnly: true
        duration_minutes:
          type: integer
          readOnly: true
        provider_name:
          type: string
          description: |-
            Return the provider's display name as ``"First Last"``.

            Staff endpoints render ``"Last, First"`` for sorting; the patient
            portal renders ``"First Last"`` because the SPA reads
            ``provider_name`` as a sentence ("with Dr. Smith") rather than a
            sortable column.  Returns an empty string when no provider is
            assigned -- the SPA treats falsy values as "no provider yet".
          readOnly: true
        procedure_description:
          type: string
          description: |-
            Patient-visible appointment summary.

            Falls back to ``appointment_type`` (e.g. "New Patient Exam") when
            ``procedure_description`` is blank so the SPA always has something
            meaningful to render.  Both fields are populated by the source
            PMS (Eaglesoft / Open Dental) and are deliberately the
            non-clinical surfaces -- intra-op notes live elsewhere.
          readOnly: true
        status:
          type: string
          description: |-
            Collapse the staff-side status taxonomy to the SPA's small set.

            Unknown / non-real statuses (``note``, ``other``, ``planned``,
            ``unscheduled``) intentionally omit a mapping -- callers should
            skip those rows server-side via the queryset filter.  If one
            slips through we render ``"scheduled"`` because the SPA's
            ``statusVariant`` helper does the same fallback.
          readOnly: true
      required:
        - duration_minutes
        - procedure_description
        - provider_name
        - start_time
        - status
        - uid
    PortalBalance:
      type: object
      description: |-
        Patient-facing balance summary shape.

        All money fields are ``Decimal`` rendered as strings to match the
        rest of the portal's money handling.  ``last_payment_date`` and
        ``last_payment_amount`` are null when the patient has never made
        a payment in this practice.
      properties:
        total_balance:
          type: string
          format: decimal
          pattern: ^-?\d{0,10}(?:\.\d{0,2})?$
          readOnly: true
        current_charges:
          type: string
          format: decimal
          pattern: ^-?\d{0,10}(?:\.\d{0,2})?$
          readOnly: true
        overdue_charges:
          type: string
          format: decimal
          pattern: ^-?\d{0,10}(?:\.\d{0,2})?$
          readOnly: true
        last_payment_date:
          type: string
          format: date
          readOnly: true
          nullable: true
        last_payment_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
          nullable: true
      required:
        - current_charges
        - last_payment_amount
        - last_payment_date
        - overdue_charges
        - total_balance
    PortalDashboardCounts:
      type: object
      description: |-
        Batched badge counts for the patient-portal dashboard.

        Backs ``GET /api/v1/portal/dashboard-counts/``.  Each integer collapses
        one of the dashboard's count-only data needs into a single cheap query
        so the home screen does not have to fetch (and the wire does not have
        to carry) the full underlying list just to derive a badge.

        Today the dashboard only renders a single badge -- "upcoming
        appointments" -- on the Appointments card.  Future badges (unread
        secure messages, outstanding forms, etc.) will land here as new
        integer fields once the matching data surfaces exist; adding a field
        is a backwards-compatible change for the SPA.

        The response carries no PHI -- only counts -- so a leak via a
        misconfigured cache or proxy is at most an integer disclosure, not a
        record disclosure.
      properties:
        upcoming_appointments:
          type: integer
          minimum: 0
          description: >-
            Count of the authenticated patient's appointments whose start time
            is at or after the current practice-local calendar date, excluding
            cancelled / completed / no-show rows.  Mirrors the dashboard's old
            client-side filter so the badge number stays identical across the
            swap.
      required:
        - upcoming_appointments
    PortalMessage:
      type: object
      description: Serializer for portal messages.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        practice:
          type: string
          format: uuid
          readOnly: true
        subject:
          type: string
          maxLength: 255
        body:
          type: string
        is_from_patient:
          type: boolean
          readOnly: true
        is_read:
          type: boolean
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - body
        - created
        - id
        - is_from_patient
        - patient
        - practice
        - subject
    PortalMessageRequest:
      type: object
      description: Serializer for portal messages.
      properties:
        subject:
          type: string
          minLength: 1
          maxLength: 255
        body:
          type: string
          minLength: 1
        is_read:
          type: boolean
        custom_data: {}
      required:
        - body
        - subject
    PortalPaymentRead:
      type: object
      description: |-
        Read-only representation of a PaymentRecord for the patient portal.

        Intentionally a plain Serializer (not ModelSerializer) to avoid
        leaking internal fields like ``provider``, ``custom_data``, etc.

        ``payment_method`` / ``last_four`` / ``receipt_url`` are placeholders
        for fields the upcoming Stripe integration (api#56) will populate.
        They are exposed today as empty strings / null so the portal UI can
        bind to them without a future schema break.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        pay_date:
          type: string
          format: date
          readOnly: true
        payment_date:
          type: string
          format: date
          readOnly: true
        pay_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        pay_type:
          type: string
          readOnly: true
        pay_note:
          type: string
          readOnly: true
        payment_method:
          type: string
          description: >-
            Return the coarse payment-method bucket (cash/check/card/...).


            Maps the raw ``pay_type`` (e.g. ``"Visa"``,
            ``"online_patient_portal"``)

            through the same classifier the staff billing serializer uses so the

            portal and staff views stay in sync.
          readOnly: true
        last_four:
          type: string
          readOnly: true
        receipt_url:
          type: string
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
      required:
        - amount
        - created
        - id
        - last_four
        - pay_amount
        - pay_date
        - pay_note
        - pay_type
        - payment_date
        - payment_method
        - receipt_url
    PortalPaymentWriteRequest:
      type: object
      description: |-
        Input for posting a patient payment via the portal.

        The ``payment_method`` field is accepted but not processed until
        Stripe integration is complete (BAA pending).
      properties:
        amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          description: Payment amount in dollars (e.g. 150.00).
        payment_method:
          type: string
          default: ''
          description: Stripe payment method token (not processed yet).
          maxLength: 255
        notes:
          type: string
          default: ''
          description: Optional note for the payment (e.g. 'Co-pay for cleaning').
          maxLength: 500
      required:
        - amount
    Practice:
      type: object
      description: Full representation of a dental practice.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        client:
          type: string
          format: uuid
          nullable: true
        client_name:
          type: string
          readOnly: true
        name:
          type: string
          maxLength: 255
        address_line1:
          type: string
          maxLength: 255
        address_line2:
          type: string
          maxLength: 255
        city:
          type: string
          maxLength: 100
        state:
          type: string
          maxLength: 2
        zip_code:
          type: string
          pattern: ^\d{5}(-\d{4})?$
          maxLength: 10
        phone:
          type: string
          maxLength: 128
        timezone:
          type: string
          maxLength: 50
        settings:
          description: Practice-specific configuration.
        production_goal_daily:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Daily production goal in USD. Null = no goal set.
        production_goal_weekly:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Weekly production goal in USD. Null = no goal set.
        production_goal_monthly:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Monthly production goal in USD. Null = no goal set.
        is_active:
          type: boolean
        logo_url:
          type: string
          readOnly: true
        client_logo_url:
          type: string
          readOnly: true
        effective_logo_url:
          type: string
          readOnly: true
        client_accent_color:
          type: string
          description: >-
            Brand accent colour inherited from the owning Client as a
            7-character ``#RRGGBB`` hex string.  Surfaced here so the staff web
            app can resolve branding from a single ``/practice/available/``
            round-trip instead of also fetching ``/client/``.  Falls back to the
            Escape default (``#0F6FFF``) for orphaned practices.
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
        active_banners:
          type: string
          readOnly: true
        inbound_document_email:
          type: string
          nullable: true
          description: >-
            Fully-qualified inbound scan-to-email / forwarded-fax address for
            this practice (e.g. ``pearl-d@escp.email``).  ``null`` until the
            practice provisions a ``ScanToEmailConfig`` row, or when the config
            exists but is disabled.
          readOnly: true
      required:
        - active_banners
        - client_accent_color
        - client_logo_url
        - client_name
        - created
        - effective_logo_url
        - id
        - inbound_document_email
        - logo_url
        - modified
        - name
    PracticeAdjustmentWrite:
      type: object
      description: |-
        Write serializer for the practice-wide adjustment endpoint.

        Unlike the patient-scoped serializer, ``patient`` must be provided in
        the request body because there is no ``patient_id`` in the URL.
      properties:
        patient:
          type: string
          format: uuid
        adj_date:
          type: string
          format: date
          nullable: true
        adj_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        adj_type:
          type: string
          maxLength: 100
        adj_note:
          type: string
        provider:
          type: string
          format: uuid
          nullable: true
        custom_data: {}
      required:
        - patient
    PracticeAdjustmentWriteRequest:
      type: object
      description: |-
        Write serializer for the practice-wide adjustment endpoint.

        Unlike the patient-scoped serializer, ``patient`` must be provided in
        the request body because there is no ``patient_id`` in the URL.
      properties:
        patient:
          type: string
          format: uuid
        adj_date:
          type: string
          format: date
          nullable: true
        adj_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        adj_type:
          type: string
          maxLength: 100
        adj_note:
          type: string
        provider:
          type: string
          format: uuid
          nullable: true
        custom_data: {}
      required:
        - patient
    PracticeExportCountsResponse:
      type: object
      description: Lightweight preview from ``GET /sync/export/counts/``.
      properties:
        practice_name:
          type: string
        counts:
          type: object
          additionalProperties:
            type: integer
        total_records:
          type: integer
      required:
        - counts
        - practice_name
        - total_records
    PracticeFaxNumber:
      type: object
      description: |-
        Read serializer for a practice's fax-number history.

        Surfaced on the practice's faxing dashboard so staff can see which
        number is currently active and which have been retired.  Write is
        intentionally not exposed — provisioning happens via Helm bootstrap
        or an internal support tool today (see issue body's "out of scope").
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        e164_number:
          type: string
          readOnly: true
          description: E.164 number Sinch provisioned (e.g. ``+15555550100``).
        sinch_number_id:
          type: string
          readOnly: true
          description: >-
            Sinch ``numbers`` resource id — used to release the number on
            deprovision.
        provisioned_at:
          type: string
          format: date-time
          readOnly: true
        deactivated_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: >-
            When the number was released back to Sinch.  NULL for the active
            number.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - deactivated_at
        - e164_number
        - id
        - modified
        - provisioned_at
        - sinch_number_id
    PracticeInvitationCreate:
      type: object
      description: >-
        Write serializer for creating a practice invitation.


        Does not create the invitation directly — the view handles token
        generation and persistence.
      properties:
        email:
          type: string
          format: email
        role:
          type: string
      required:
        - email
        - role
    PracticeInvitationCreateRequest:
      type: object
      description: >-
        Write serializer for creating a practice invitation.


        Does not create the invitation directly — the view handles token
        generation and persistence.
      properties:
        email:
          type: string
          format: email
          minLength: 1
        role:
          type: string
          minLength: 1
      required:
        - email
        - role
    PracticeInvitationList:
      type: object
      description: Read-only serializer for listing practice invitations.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        email:
          type: string
          format: email
          readOnly: true
          description: Invited user's email (normalized to lowercase)
        role:
          allOf:
            - $ref: '#/components/schemas/MemberRoleEnum'
          readOnly: true
        status:
          allOf:
            - $ref: '#/components/schemas/PracticeInvitationListStatusEnum'
          readOnly: true
        email_status:
          allOf:
            - $ref: '#/components/schemas/EmailStatusEnum'
          readOnly: true
        expires_at:
          type: string
          format: date-time
          readOnly: true
        invited_at:
          type: string
          format: date-time
          readOnly: true
        invited_by_name:
          type: string
          readOnly: true
      required:
        - email
        - email_status
        - expires_at
        - id
        - invited_at
        - invited_by_name
        - role
        - status
    PracticeInvitationListStatusEnum:
      enum:
        - PENDING
        - ACCEPTED
        - REVOKED
        - EXPIRED
      type: string
      description: |-
        * `PENDING` - Pending
        * `ACCEPTED` - Accepted
        * `REVOKED` - Revoked
        * `EXPIRED` - Expired
    PracticeMember:
      type: object
      description: >-
        Read-only serializer for practice membership list (staff management
        view).


        Emits both ``via_practice`` and ``via_client`` discriminators so the
        staff

        UI can tell direct practice members from client-level admins projected
        into

        the same response (see :class:`ClientMemberAsPracticeMemberSerializer`).


        Defaults: ``via_practice=True`` (this row was sourced from a real

        :class:`PracticeMembership`) and ``via_client=False``.  When the same
        user

        *also* holds a :class:`ClientMembership` on the practice's parent
        client,

        the list view (api#570) sets ``via_client=True`` on this same row via

        ``context["users_with_client_access"]`` so the UI can render

        "Direct + Inherited from organization" without a duplicate row.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        user_id:
          type: string
          format: uuid
          readOnly: true
        email:
          type: string
          readOnly: true
        first_name:
          type: string
          readOnly: true
        last_name:
          type: string
          readOnly: true
        role:
          allOf:
            - $ref: '#/components/schemas/MemberRoleEnum'
          readOnly: true
        is_active:
          type: boolean
          readOnly: true
        date_joined:
          type: string
          readOnly: true
        invited_by_name:
          type: string
          readOnly: true
        via_practice:
          type: string
          readOnly: true
        via_client:
          type: string
          readOnly: true
      required:
        - date_joined
        - email
        - first_name
        - id
        - invited_by_name
        - is_active
        - last_name
        - role
        - user_id
        - via_client
        - via_practice
    PracticeMemberUpdate:
      type: object
      description: >-
        Write serializer for updating a practice membership (role + active
        status).
      properties:
        role:
          $ref: '#/components/schemas/MemberRoleEnum'
        is_active:
          type: boolean
      required:
        - role
    PracticeMemberUpdateRequest:
      type: object
      description: >-
        Write serializer for updating a practice membership (role + active
        status).
      properties:
        role:
          $ref: '#/components/schemas/MemberRoleEnum'
        is_active:
          type: boolean
      required:
        - role
    PracticeMembership:
      type: object
      description: Serializer for a user's practice memberships (nested in UserSerializer).
      properties:
        practice_id:
          type: string
          format: uuid
        practice_name:
          type: string
        client_id:
          type: string
          format: uuid
        role:
          $ref: '#/components/schemas/MemberRoleEnum'
        is_active:
          type: boolean
      required:
        - client_id
        - practice_id
        - practice_name
        - role
    PracticeMembershipRequest:
      type: object
      description: Serializer for a user's practice memberships (nested in UserSerializer).
      properties:
        practice_id:
          type: string
          format: uuid
        practice_name:
          type: string
          minLength: 1
        client_id:
          type: string
          format: uuid
        role:
          $ref: '#/components/schemas/MemberRoleEnum'
        is_active:
          type: boolean
      required:
        - client_id
        - practice_id
        - practice_name
        - role
    PracticePaymentWrite:
      type: object
      description: |-
        Write serializer for the practice-wide payment endpoint.

        Unlike the patient-scoped serializer, ``patient`` must be provided in
        the request body because there is no ``patient_id`` in the URL.

        GH-711: the ``patient`` and ``provider`` FKs are validated against
        the URL practice so a first-party bulk POST can't drop a payment row
        into another tenant's nested table.
      properties:
        patient:
          type: string
          format: uuid
        pay_date:
          type: string
          format: date
          nullable: true
        pay_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        pay_type:
          type: string
          maxLength: 100
        pay_note:
          type: string
        provider:
          type: string
          format: uuid
          nullable: true
        custom_data: {}
        splits:
          type: array
          items:
            $ref: '#/components/schemas/PaymentSplitWrite'
      required:
        - patient
    PracticePaymentWriteRequest:
      type: object
      description: |-
        Write serializer for the practice-wide payment endpoint.

        Unlike the patient-scoped serializer, ``patient`` must be provided in
        the request body because there is no ``patient_id`` in the URL.

        GH-711: the ``patient`` and ``provider`` FKs are validated against
        the URL practice so a first-party bulk POST can't drop a payment row
        into another tenant's nested table.
      properties:
        patient:
          type: string
          format: uuid
        pay_date:
          type: string
          format: date
          nullable: true
        pay_amount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        pay_type:
          type: string
          maxLength: 100
        pay_note:
          type: string
        provider:
          type: string
          format: uuid
          nullable: true
        custom_data: {}
        splits:
          type: array
          items:
            $ref: '#/components/schemas/PaymentSplitWriteRequest'
      required:
        - patient
    PracticeRequest:
      type: object
      description: Full representation of a dental practice.
      properties:
        client:
          type: string
          format: uuid
          nullable: true
        name:
          type: string
          minLength: 1
          maxLength: 255
        address_line1:
          type: string
          maxLength: 255
        address_line2:
          type: string
          maxLength: 255
        city:
          type: string
          maxLength: 100
        state:
          type: string
          maxLength: 2
        zip_code:
          type: string
          pattern: ^\d{5}(-\d{4})?$
          maxLength: 10
        phone:
          type: string
          maxLength: 128
        timezone:
          type: string
          minLength: 1
          maxLength: 50
        settings:
          description: Practice-specific configuration.
        production_goal_daily:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Daily production goal in USD. Null = no goal set.
        production_goal_weekly:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Weekly production goal in USD. Null = no goal set.
        production_goal_monthly:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          nullable: true
          description: Monthly production goal in USD. Null = no goal set.
        is_active:
          type: boolean
        custom_data: {}
      required:
        - name
    PracticeSMSNumber:
      type: object
      description: Read serializer for a practice's SMS-number history.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        e164_number:
          type: string
          readOnly: true
          description: E.164 number Sinch provisioned (e.g. ``+15555550100``).
        sinch_number_id:
          type: string
          readOnly: true
          description: >-
            Sinch ``numbers`` resource id -- used to release the number on
            deprovision.
        provisioned_at:
          type: string
          format: date-time
          readOnly: true
        deactivated_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: >-
            When the number was released back to Sinch.  NULL for the active
            number.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - deactivated_at
        - e164_number
        - id
        - modified
        - provisioned_at
        - sinch_number_id
    PracticeUserGrantCreate:
      type: object
      description: |-
        Write serializer for creating a per-practice user grant.

        Accepts ``user`` (UUID) and ``permission`` (one of the
        :class:`GrantPermission` choices); ``granted`` defaults to True so
        the common "give this user the bit" call doesn't need a body field
        just to flip the verb on.  ``note`` is optional free-text the
        granter can record for audit.

        The view layer is responsible for setting ``practice`` from the URL
        and ``granted_by`` from ``request.user``; both are excluded here so
        a malicious body cannot relocate the grant onto another practice or
        forge a different granter.
      properties:
        user:
          type: string
          format: uuid
        permission:
          $ref: '#/components/schemas/PermissionEnum'
        granted:
          type: boolean
          default: true
          description: True forces the permission ON; False forces it OFF.
        note:
          type: string
          description: Optional reason for the grant (e.g. 'covering for OM on leave').
      required:
        - permission
        - user
    PracticeUserGrantCreateRequest:
      type: object
      description: |-
        Write serializer for creating a per-practice user grant.

        Accepts ``user`` (UUID) and ``permission`` (one of the
        :class:`GrantPermission` choices); ``granted`` defaults to True so
        the common "give this user the bit" call doesn't need a body field
        just to flip the verb on.  ``note`` is optional free-text the
        granter can record for audit.

        The view layer is responsible for setting ``practice`` from the URL
        and ``granted_by`` from ``request.user``; both are excluded here so
        a malicious body cannot relocate the grant onto another practice or
        forge a different granter.
      properties:
        user:
          type: string
          format: uuid
        permission:
          $ref: '#/components/schemas/PermissionEnum'
        granted:
          type: boolean
          default: true
          description: True forces the permission ON; False forces it OFF.
        note:
          type: string
          description: Optional reason for the grant (e.g. 'covering for OM on leave').
      required:
        - permission
        - user
    PracticeUserGrantList:
      type: object
      description: |-
        Read serializer for the per-practice user-grant list.

        Surfaces the user identity and the granter's display name so the
        staff UI can render a row like "alice@foo gave can_share to bob@bar"
        without needing a follow-up lookup.  ``permission`` is the canonical
        string from :class:`GrantPermission`; ``granted`` is the resolved
        ON/OFF boolean.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        user:
          type: string
          format: uuid
          readOnly: true
        user_email:
          type: string
          format: email
          readOnly: true
        user_first_name:
          type: string
          readOnly: true
        user_last_name:
          type: string
          readOnly: true
        permission:
          allOf:
            - $ref: '#/components/schemas/PermissionEnum'
          readOnly: true
          description: |-
            The permission being granted or revoked.

            * `patient.can_share` - Patient: Can Share
            * `reports.all` - Reports: All Categories
            * `reports.production` - Reports: Production
            * `reports.ar` - Reports: A/R
            * `reports.patients` - Reports: Patients
            * `reports.operations` - Reports: Operations
            * `reports.compliance` - Reports: Compliance
        granted:
          type: boolean
          readOnly: true
          description: True forces the permission ON; False forces it OFF.
        granted_by_email:
          type: string
          nullable: true
          readOnly: true
        granted_at:
          type: string
          format: date-time
          readOnly: true
        note:
          type: string
          readOnly: true
          description: Optional reason for the grant (e.g. 'covering for OM on leave').
      required:
        - granted
        - granted_at
        - granted_by_email
        - id
        - note
        - permission
        - user
        - user_email
        - user_first_name
        - user_last_name
    Preference:
      type: object
      description: Exposes preference data without user/application fields.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        data: {}
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - id
        - modified
    PreferenceRequest:
      type: object
      description: Exposes preference data without user/application fields.
      properties:
        data: {}
    Prescription:
      type: object
      description: >-
        Prescription serializer.


        ``patient_name`` and ``provider_name`` are pre-joined read-only fields
        for

        display without a second round-trip.  ``is_controlled`` is a derived

        boolean that the web form uses to trigger the PDMP reminder.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
        patient_name:
          type: string
          nullable: true
          readOnly: true
        provider:
          type: string
          format: uuid
          nullable: true
          description: The prescribing/responsible provider. Required to finalise.
        provider_name:
          type: string
          nullable: true
          readOnly: true
        drug_name:
          type: string
          description: e.g. Amoxicillin
          maxLength: 200
        drug_strength:
          type: string
          description: e.g. 500mg
          maxLength: 100
        drug_form:
          type: string
          description: 'Dosage form: Capsule, Tablet, Solution, Cream, etc.'
          maxLength: 100
        quantity:
          type: string
          description: 'e.g. #21 or 30 mL'
          maxLength: 50
        directions:
          type: string
          description: >-
            Sig / patient instructions e.g. 'Take 1 capsule by mouth three times
            daily for 7 days'
        refills:
          type: integer
          maximum: 32767
          minimum: 0
          description: >-
            Number of refills (0 = no refills). Controlled substances may not be
            refilled in many states.
        substitution_permitted:
          type: boolean
          description: Allow pharmacist to substitute generic equivalent.
        cs_schedule:
          allOf:
            - $ref: '#/components/schemas/CsScheduleEnum'
          description: >-
            DEA Controlled Substance Schedule. Non-Controlled for the vast
            majority of dental prescriptions (antibiotics, NSAIDs, fluoride,
            chlorhexidine). Use Schedule II–V only for opioids, benzodiazepines,
            etc.


            * `none` - Non-Controlled

            * `II` - Schedule II

            * `III` - Schedule III

            * `IV` - Schedule IV

            * `V` - Schedule V
        is_controlled:
          type: boolean
          readOnly: true
        diagnosis_description:
          type: string
          description: Brief clinical indication e.g. 'Post-extraction infection'.
          maxLength: 255
        appointment:
          type: string
          format: uuid
          nullable: true
        status:
          allOf:
            - $ref: '#/components/schemas/PrescriptionStatusEnum'
          readOnly: true
        finalized_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        finalized_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: >-
            The user (must hold a provider role) who finalised this
            prescription.
        finalized_by_name:
          type: string
          nullable: true
          readOnly: true
        notes:
          type: string
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - directions
        - drug_name
        - finalized_at
        - finalized_by
        - finalized_by_name
        - id
        - is_controlled
        - modified
        - patient
        - patient_name
        - provider_name
        - status
    PrescriptionRequest:
      type: object
      description: >-
        Prescription serializer.


        ``patient_name`` and ``provider_name`` are pre-joined read-only fields
        for

        display without a second round-trip.  ``is_controlled`` is a derived

        boolean that the web form uses to trigger the PDMP reminder.
      properties:
        patient:
          type: string
          format: uuid
        provider:
          type: string
          format: uuid
          nullable: true
          description: The prescribing/responsible provider. Required to finalise.
        drug_name:
          type: string
          minLength: 1
          description: e.g. Amoxicillin
          maxLength: 200
        drug_strength:
          type: string
          description: e.g. 500mg
          maxLength: 100
        drug_form:
          type: string
          description: 'Dosage form: Capsule, Tablet, Solution, Cream, etc.'
          maxLength: 100
        quantity:
          type: string
          description: 'e.g. #21 or 30 mL'
          maxLength: 50
        directions:
          type: string
          minLength: 1
          description: >-
            Sig / patient instructions e.g. 'Take 1 capsule by mouth three times
            daily for 7 days'
        refills:
          type: integer
          maximum: 32767
          minimum: 0
          description: >-
            Number of refills (0 = no refills). Controlled substances may not be
            refilled in many states.
        substitution_permitted:
          type: boolean
          description: Allow pharmacist to substitute generic equivalent.
        cs_schedule:
          allOf:
            - $ref: '#/components/schemas/CsScheduleEnum'
          description: >-
            DEA Controlled Substance Schedule. Non-Controlled for the vast
            majority of dental prescriptions (antibiotics, NSAIDs, fluoride,
            chlorhexidine). Use Schedule II–V only for opioids, benzodiazepines,
            etc.


            * `none` - Non-Controlled

            * `II` - Schedule II

            * `III` - Schedule III

            * `IV` - Schedule IV

            * `V` - Schedule V
        diagnosis_description:
          type: string
          description: Brief clinical indication e.g. 'Post-extraction infection'.
          maxLength: 255
        appointment:
          type: string
          format: uuid
          nullable: true
        notes:
          type: string
      required:
        - directions
        - drug_name
        - patient
    PrescriptionStatusEnum:
      enum:
        - draft
        - final
        - cancelled
      type: string
      description: |-
        * `draft` - Draft
        * `final` - Final
        * `cancelled` - Cancelled
    PrescriptionTemplate:
      type: object
      description: Read/write serializer for practice prescription templates.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          description: >-
            Template display name shown in the picker e.g. 'Amoxicillin 500mg
            #21'
          maxLength: 200
        drug_name:
          type: string
          maxLength: 200
        drug_strength:
          type: string
          maxLength: 100
        drug_form:
          type: string
          maxLength: 100
        quantity:
          type: string
          maxLength: 50
        directions:
          type: string
        refills:
          type: integer
          maximum: 32767
          minimum: 0
        substitution_permitted:
          type: boolean
        cs_schedule:
          $ref: '#/components/schemas/CsScheduleEnum'
        is_active:
          type: boolean
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - directions
        - drug_name
        - id
        - modified
        - name
    PrescriptionTemplateRequest:
      type: object
      description: Read/write serializer for practice prescription templates.
      properties:
        name:
          type: string
          minLength: 1
          description: >-
            Template display name shown in the picker e.g. 'Amoxicillin 500mg
            #21'
          maxLength: 200
        drug_name:
          type: string
          minLength: 1
          maxLength: 200
        drug_strength:
          type: string
          maxLength: 100
        drug_form:
          type: string
          maxLength: 100
        quantity:
          type: string
          maxLength: 50
        directions:
          type: string
          minLength: 1
        refills:
          type: integer
          maximum: 32767
          minimum: 0
        substitution_permitted:
          type: boolean
        cs_schedule:
          $ref: '#/components/schemas/CsScheduleEnum'
        is_active:
          type: boolean
      required:
        - directions
        - drug_name
        - name
    Procedure:
      type: object
      description: |-
        Read serializer for procedures with denormalized provider name.

        Exposes both the legacy string ``proc_code`` (authoritative for PMS
        imports where no canonical ProcedureCode match exists) and the nested
        ``procedure_code`` FK object (populated when the resolver found a
        matching :class:`ProcedureCode` in the practice).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        provider:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        provider_name:
          type: string
          nullable: true
          readOnly: true
        appointment:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        procedure_code:
          allOf:
            - $ref: '#/components/schemas/ProcedureCodeNested'
          readOnly: true
        proc_code:
          type: string
          readOnly: true
        description:
          type: string
          readOnly: true
        proc_date:
          type: string
          format: date
          readOnly: true
          nullable: true
        date_complete:
          type: string
          format: date
          readOnly: true
          nullable: true
        proc_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        status:
          type: string
          readOnly: true
          description: complete, existing_current, existing_other
        tooth_num:
          type: string
          readOnly: true
        surface:
          type: string
          readOnly: true
        discount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        unit_qty:
          type: integer
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - appointment
        - created
        - date_complete
        - description
        - discount
        - id
        - modified
        - patient
        - proc_code
        - proc_date
        - proc_fee
        - procedure_code
        - provider
        - provider_name
        - status
        - surface
        - tooth_num
        - unit_qty
    ProcedureBulkWrite:
      type: object
      description: >-
        Write serializer for practice-wide bulk procedure import.


        Includes ``patient`` (required) since the endpoint is not
        patient-scoped.


        The bulk create path (``POST`` with a JSON array) is the hot path for

        PMS sync tools (escape-opendental, escape-eaglesoft, escape-backup).

        Like :class:`ProcedureWriteSerializer`, this serializer resolves

        ``proc_code`` into the ``procedure_code`` FK and backfills ``proc_fee``

        via :func:`_resolve_procedure_code_fk` on every create/update, so

        imported rows are properly linked to the practice's canonical codes

        without requiring a follow-up migration.


        GH-711: cross-practice FK rejection via

        :class:`apps.common.bulk.PracticeFKValidatorMixin`.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
        provider:
          type: string
          format: uuid
          nullable: true
        appointment:
          type: string
          format: uuid
          nullable: true
        procedure_code:
          type: string
          format: uuid
          nullable: true
        proc_code:
          type: string
          maxLength: 20
        description:
          type: string
          maxLength: 255
        proc_date:
          type: string
          format: date
          nullable: true
        date_complete:
          type: string
          format: date
          nullable: true
        proc_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        status:
          type: string
          description: complete, existing_current, existing_other
          maxLength: 30
        tooth_num:
          type: string
          maxLength: 10
        surface:
          type: string
          maxLength: 20
        discount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        unit_qty:
          type: integer
          maximum: 2147483647
          minimum: 0
        custom_data: {}
      required:
        - id
        - patient
    ProcedureBulkWriteRequest:
      type: object
      description: >-
        Write serializer for practice-wide bulk procedure import.


        Includes ``patient`` (required) since the endpoint is not
        patient-scoped.


        The bulk create path (``POST`` with a JSON array) is the hot path for

        PMS sync tools (escape-opendental, escape-eaglesoft, escape-backup).

        Like :class:`ProcedureWriteSerializer`, this serializer resolves

        ``proc_code`` into the ``procedure_code`` FK and backfills ``proc_fee``

        via :func:`_resolve_procedure_code_fk` on every create/update, so

        imported rows are properly linked to the practice's canonical codes

        without requiring a follow-up migration.


        GH-711: cross-practice FK rejection via

        :class:`apps.common.bulk.PracticeFKValidatorMixin`.
      properties:
        patient:
          type: string
          format: uuid
        provider:
          type: string
          format: uuid
          nullable: true
        appointment:
          type: string
          format: uuid
          nullable: true
        procedure_code:
          type: string
          format: uuid
          nullable: true
        proc_code:
          type: string
          maxLength: 20
        description:
          type: string
          maxLength: 255
        proc_date:
          type: string
          format: date
          nullable: true
        date_complete:
          type: string
          format: date
          nullable: true
        proc_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        status:
          type: string
          description: complete, existing_current, existing_other
          maxLength: 30
        tooth_num:
          type: string
          maxLength: 10
        surface:
          type: string
          maxLength: 20
        discount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        unit_qty:
          type: integer
          maximum: 2147483647
          minimum: 0
        custom_data: {}
      required:
        - patient
    ProcedureCode:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        code:
          type: string
          maxLength: 20
        description:
          type: string
          maxLength: 255
        abbreviation:
          type: string
          maxLength: 100
        service_type:
          oneOf:
            - $ref: '#/components/schemas/ServiceTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        default_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          description: Practice UCR fee
        default_duration_minutes:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Default appointment block size
        generate_recall:
          type: boolean
          description: Triggers recall scheduling when completed
        is_active:
          type: boolean
        ada_code:
          type: string
          description: ADA/CDT code reference (insurance module)
          maxLength: 20
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - code
        - created
        - id
        - modified
    ProcedureCodeNested:
      type: object
      description: >-
        Compact read-only representation of a :class:`clinical.ProcedureCode`.


        Mirrors the serializer of the same name in
        :mod:`apps.insurance.serializers`

        and :mod:`apps.clinical.serializers`; duplicated here rather than
        imported

        cross-app so the billing serializer module remains self-contained.  Used
        to

        render the resolved ``procedure_code`` FK as a nested object on

        :class:`PaymentSplit` reads without a second round-trip.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        code:
          type: string
          readOnly: true
        description:
          type: string
          readOnly: true
        abbreviation:
          type: string
          readOnly: true
      required:
        - abbreviation
        - code
        - description
        - id
    ProcedureCodeRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        code:
          type: string
          minLength: 1
          maxLength: 20
        description:
          type: string
          maxLength: 255
        abbreviation:
          type: string
          maxLength: 100
        service_type:
          oneOf:
            - $ref: '#/components/schemas/ServiceTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        default_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          description: Practice UCR fee
        default_duration_minutes:
          type: integer
          maximum: 2147483647
          minimum: 0
          description: Default appointment block size
        generate_recall:
          type: boolean
          description: Triggers recall scheduling when completed
        is_active:
          type: boolean
        ada_code:
          type: string
          description: ADA/CDT code reference (insurance module)
          maxLength: 20
        custom_data: {}
      required:
        - code
    ProcedureWrite:
      type: object
      description: >-
        Write serializer for creating and updating procedures.


        The ``patient`` field is excluded because it is set from the URL path

        by the view's ``perform_create`` method.


        On ``create``/``update``, resolves the ``proc_code`` string into the

        ``procedure_code`` FK (practice-scoped) and backfills ``proc_fee`` from

        ``ProcedureCode.default_fee`` when the caller sent no non-zero fee.

        Without this, PMS imports (which send only the string) leave the FK
        NULL.

        See :func:`_resolve_procedure_code_fk` for the shared logic.


        GH-711: ``patient`` / ``provider`` / ``appointment`` /
        ``procedure_code``

        FKs are validated against the URL practice via

        :class:`apps.common.bulk.PracticeFKValidatorMixin`.  Used directly by

        :class:`apps.sync.views_bulk.BulkProcedureView` (with ``patient``

        injected by ``make_bulk_serializer``).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        provider:
          type: string
          format: uuid
          nullable: true
        appointment:
          type: string
          format: uuid
          nullable: true
        procedure_code:
          type: string
          format: uuid
          nullable: true
        proc_code:
          type: string
          maxLength: 20
        description:
          type: string
          maxLength: 255
        proc_date:
          type: string
          format: date
          nullable: true
        proc_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        status:
          type: string
          description: complete, existing_current, existing_other
          maxLength: 30
        tooth_num:
          type: string
          maxLength: 10
        surface:
          type: string
          maxLength: 20
        discount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        unit_qty:
          type: integer
          maximum: 2147483647
          minimum: 0
        custom_data: {}
      required:
        - id
    ProcedureWriteRequest:
      type: object
      description: >-
        Write serializer for creating and updating procedures.


        The ``patient`` field is excluded because it is set from the URL path

        by the view's ``perform_create`` method.


        On ``create``/``update``, resolves the ``proc_code`` string into the

        ``procedure_code`` FK (practice-scoped) and backfills ``proc_fee`` from

        ``ProcedureCode.default_fee`` when the caller sent no non-zero fee.

        Without this, PMS imports (which send only the string) leave the FK
        NULL.

        See :func:`_resolve_procedure_code_fk` for the shared logic.


        GH-711: ``patient`` / ``provider`` / ``appointment`` /
        ``procedure_code``

        FKs are validated against the URL practice via

        :class:`apps.common.bulk.PracticeFKValidatorMixin`.  Used directly by

        :class:`apps.sync.views_bulk.BulkProcedureView` (with ``patient``

        injected by ``make_bulk_serializer``).
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        appointment:
          type: string
          format: uuid
          nullable: true
        procedure_code:
          type: string
          format: uuid
          nullable: true
        proc_code:
          type: string
          maxLength: 20
        description:
          type: string
          maxLength: 255
        proc_date:
          type: string
          format: date
          nullable: true
        proc_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        status:
          type: string
          description: complete, existing_current, existing_other
          maxLength: 30
        tooth_num:
          type: string
          maxLength: 10
        surface:
          type: string
          maxLength: 20
        discount:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        unit_qty:
          type: integer
          maximum: 2147483647
          minimum: 0
        custom_data: {}
    ProcessingStateEnum:
      enum:
        - pending
        - decoding
        - loading_model
        - processing
        - indexing
        - complete
        - failed
      type: string
      description: |-
        * `pending` - Pending
        * `decoding` - Decoding
        * `loading_model` - Loading model
        * `processing` - Processing
        * `indexing` - Indexing
        * `complete` - Complete
        * `failed` - Failed
    ProductionByProviderPracticeRow:
      type: object
      description: Per-practice provider rollup.
      properties:
        practice_id:
          type: string
        practice_name:
          type: string
        providers:
          type: array
          items:
            $ref: '#/components/schemas/_ProviderProductionRow'
      required:
        - practice_id
        - practice_name
        - providers
    ProductionByProviderResponse:
      type: object
      description: Response for ``/reports/production-by-provider/``.
      properties:
        date_from:
          type: string
        date_to:
          type: string
        practices:
          type: array
          items:
            $ref: '#/components/schemas/ProductionByProviderPracticeRow'
      required:
        - date_from
        - date_to
        - practices
    ProductionReportProcedureRow:
      type: object
      description: Per-procedure-code row in the production report.
      properties:
        proc_code:
          type: string
        description:
          type: string
        count:
          type: integer
        total_production:
          type: string
        avg_fee:
          type: string
      required:
        - avg_fee
        - count
        - description
        - proc_code
        - total_production
    ProductionReportProviderRow:
      type: object
      description: Per-provider row in the production report.
      properties:
        provider_id:
          type: string
          format: uuid
          nullable: true
        provider_name:
          type: string
        production:
          type: string
        completed_production:
          type: string
        scheduled_production:
          type: string
        collections:
          type: string
        adjustments:
          type: string
        procedure_count:
          type: integer
      required:
        - adjustments
        - collections
        - completed_production
        - procedure_count
        - production
        - provider_id
        - provider_name
        - scheduled_production
    ProductionReportResponse:
      type: object
      description: Response from ``GET /reports/production/``.
      properties:
        date_from:
          type: string
        date_to:
          type: string
        summary:
          $ref: '#/components/schemas/ProductionReportSummary'
        by_provider:
          type: array
          items:
            $ref: '#/components/schemas/ProductionReportProviderRow'
        by_procedure:
          type: array
          items:
            $ref: '#/components/schemas/ProductionReportProcedureRow'
      required:
        - by_procedure
        - by_provider
        - date_from
        - date_to
        - summary
    ProductionReportSummary:
      type: object
      description: Top-line totals returned by ``GET /reports/production/``.
      properties:
        total_production:
          type: string
        total_collections:
          type: string
        total_adjustments:
          type: string
        net_production:
          type: string
        procedure_count:
          type: integer
      required:
        - net_production
        - procedure_count
        - total_adjustments
        - total_collections
        - total_production
    ProductionTrendingBucket:
      type: object
      description: One bucket row in the production-trending response.
      properties:
        bucket_start:
          type: string
        bucket_end:
          type: string
        production:
          type: string
        collections:
          type: string
        adjustments:
          type: string
        net_production:
          type: string
        appointment_count:
          type: integer
      required:
        - appointment_count
        - bucket_end
        - bucket_start
    ProductionTrendingResponse:
      type: object
      description: Response from ``GET /reports/production/trending/``.
      properties:
        date_from:
          type: string
        date_to:
          type: string
        bucket:
          type: string
        metrics:
          type: array
          items:
            type: string
        rows:
          type: array
          items:
            $ref: '#/components/schemas/ProductionTrendingBucket'
        totals:
          type: object
          additionalProperties:
            type: string
      required:
        - bucket
        - date_from
        - date_to
        - metrics
        - rows
        - totals
    ProductionVsCollectionsResponse:
      type: object
      description: Response for ``/reports/production-vs-collections/``.
      properties:
        date_from:
          type: string
        date_to:
          type: string
        totals:
          $ref: '#/components/schemas/_ProductionTotals'
        practices:
          type: array
          items:
            $ref: '#/components/schemas/ProductionVsCollectionsRow'
      required:
        - date_from
        - date_to
        - practices
        - totals
    ProductionVsCollectionsRow:
      type: object
      description: Per-practice production / collections row.
      properties:
        practice_id:
          type: string
        practice_name:
          type: string
        production:
          type: string
        collections:
          type: string
        adjustments:
          type: string
        net_production:
          type: string
        collection_pct:
          type: string
      required:
        - adjustments
        - collection_pct
        - collections
        - net_production
        - practice_id
        - practice_name
        - production
    Provider:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        first_name:
          type: string
          maxLength: 100
        last_name:
          type: string
          maxLength: 100
        abbreviation:
          type: string
          maxLength: 20
        specialty:
          type: string
          maxLength: 100
        provider_type:
          $ref: '#/components/schemas/ProviderTypeEnum'
        color:
          type: string
          description: 'Hex color for calendar rendering, e.g. #3B82F6'
          maxLength: 7
        npi:
          type: string
          description: National Provider Identifier
          maxLength: 10
        dea_number:
          type: string
          description: >-
            DEA registration number (2 letters + 7 digits, e.g. AB1234567). 
            Required on paper controlled-substance prescriptions.  Not required
            for non-controlled prescriptions.  Added as part of the
            prescriptions module (escape#136 / api#482).
          maxLength: 9
        is_hidden:
          type: boolean
          description: Hidden from dropdowns but visible on historical records
        is_secondary:
          type: boolean
          description: Legacy import field — use provider_type instead
        is_active:
          type: boolean
          description: Currently employed at this practice
        is_web_sched:
          type: boolean
          description: Available for patient online scheduling.
        custom_data: {}
      required:
        - first_name
        - id
        - last_name
    ProviderRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        first_name:
          type: string
          minLength: 1
          maxLength: 100
        last_name:
          type: string
          minLength: 1
          maxLength: 100
        abbreviation:
          type: string
          maxLength: 20
        specialty:
          type: string
          maxLength: 100
        provider_type:
          $ref: '#/components/schemas/ProviderTypeEnum'
        color:
          type: string
          description: 'Hex color for calendar rendering, e.g. #3B82F6'
          maxLength: 7
        npi:
          type: string
          description: National Provider Identifier
          maxLength: 10
        dea_number:
          type: string
          description: >-
            DEA registration number (2 letters + 7 digits, e.g. AB1234567). 
            Required on paper controlled-substance prescriptions.  Not required
            for non-controlled prescriptions.  Added as part of the
            prescriptions module (escape#136 / api#482).
          maxLength: 9
        is_hidden:
          type: boolean
          description: Hidden from dropdowns but visible on historical records
        is_secondary:
          type: boolean
          description: Legacy import field — use provider_type instead
        is_active:
          type: boolean
          description: Currently employed at this practice
        is_web_sched:
          type: boolean
          description: Available for patient online scheduling.
        custom_data: {}
      required:
        - first_name
        - last_name
    ProviderTypeEnum:
      enum:
        - dentist
        - hygienist
        - assistant
        - other
      type: string
      description: |-
        * `dentist` - Dentist
        * `hygienist` - Hygienist
        * `assistant` - Assistant
        * `other` - Other
    ProviderUtilizationResponse:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/ProviderUtilizationRow'
      required:
        - results
    ProviderUtilizationRow:
      type: object
      properties:
        provider_id:
          type: string
        provider_name:
          type: string
        bucket_start:
          type: string
          format: date
        bucket_end:
          type: string
          format: date
        template_hours:
          type: string
        blocked_hours:
          type: string
        available_hours:
          type: string
        scheduled_hours:
          type: string
        open_hours:
          type: string
        utilization_pct:
          type: string
        appointment_count:
          type: integer
        production_scheduled:
          type: string
        production_completed:
          type: string
      required:
        - appointment_count
        - available_hours
        - blocked_hours
        - bucket_end
        - bucket_start
        - open_hours
        - production_completed
        - production_scheduled
        - provider_id
        - provider_name
        - scheduled_hours
        - template_hours
        - utilization_pct
    PurposeEnum:
      enum:
        - passkey_recovery
      type: string
      description: '* `passkey_recovery` - Passkey recovery'
    Recall:
      type: object
      description: >-
        Read serializer for Recall / Recare records.


        ``last_visit_date``, ``has_future_appointment`` and ``last_reached_at``

        are populated by annotations added in

        :class:`apps.scheduling.views.RecallListCreateView` (and the
        patient-scoped

        variant).  When a recall is serialized from a queryset that does not

        annotate these fields they fall back to ``None``.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        patient_name:
          type: string
          readOnly: true
        prefers_sms:
          type: boolean
          readOnly: true
        prefers_email:
          type: boolean
          readOnly: true
        recall_type:
          type: string
          readOnly: true
        date_due:
          type: string
          format: date
          readOnly: true
          nullable: true
        date_previous:
          type: string
          format: date
          readOnly: true
          nullable: true
        date_scheduled:
          type: string
          format: date
          readOnly: true
          nullable: true
        last_visit_date:
          type: string
          format: date
          readOnly: true
          nullable: true
        has_future_appointment:
          type: boolean
          readOnly: true
        last_reached_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        is_disabled:
          type: boolean
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - created
        - date_due
        - date_previous
        - date_scheduled
        - has_future_appointment
        - id
        - is_disabled
        - last_reached_at
        - last_visit_date
        - modified
        - patient
        - patient_name
        - prefers_email
        - prefers_sms
        - recall_type
    RecallTypeListResponse:
      type: object
      properties:
        results:
          type: array
          items:
            type: string
      required:
        - results
    RecallWrite:
      type: object
      description: |-
        Write serializer for Recall / Recare records.

        GH-711: ``patient`` FK must belong to URL practice on the bulk path.
      properties:
        patient:
          type: string
          format: uuid
        recall_type:
          type: string
          maxLength: 100
        date_due:
          type: string
          format: date
          nullable: true
        date_previous:
          type: string
          format: date
          nullable: true
        date_scheduled:
          type: string
          format: date
          nullable: true
        is_disabled:
          type: boolean
        custom_data: {}
      required:
        - recall_type
    RecallWriteRequest:
      type: object
      description: |-
        Write serializer for Recall / Recare records.

        GH-711: ``patient`` FK must belong to URL practice on the bulk path.
      properties:
        patient:
          type: string
          format: uuid
        recall_type:
          type: string
          minLength: 1
          maxLength: 100
        date_due:
          type: string
          format: date
          nullable: true
        date_previous:
          type: string
          format: date
          nullable: true
        date_scheduled:
          type: string
          format: date
          nullable: true
        is_disabled:
          type: boolean
        custom_data: {}
      required:
        - recall_type
    RecipientKindEnum:
      enum:
        - escape_practice
        - non_escape_treating
        - non_treating_party
        - patient
      type: string
      description: |-
        * `escape_practice` - Another Escape practice
        * `non_escape_treating` - Non-Escape treating practice
        * `non_treating_party` - Non-treating party (legal / employer / other)
        * `patient` - The patient themselves
    ReferralSource:
      type: object
      description: >-
        Read+write serializer for referral sources.


        Used for both list/retrieve and create/update. Practice is set by the
        view.

        ``custom_data`` is scoped per OAuth application.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        first_name:
          type: string
          maxLength: 100
        last_name:
          type: string
          maxLength: 100
        specialty:
          type: string
          maxLength: 100
        phone:
          type: string
          maxLength: 20
        email:
          type: string
          format: email
          maxLength: 254
        npi:
          type: string
          maxLength: 20
        is_doctor:
          type: boolean
        custom_data: {}
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - created
        - id
        - modified
    ReferralSourceWrite:
      type: object
      description: |-
        Write serializer for creating referral sources.

        Practice is set from the URL path by the view.
      properties:
        first_name:
          type: string
          maxLength: 100
        last_name:
          type: string
          maxLength: 100
        specialty:
          type: string
          maxLength: 100
        phone:
          type: string
          maxLength: 20
        email:
          type: string
          format: email
          maxLength: 254
        npi:
          type: string
          maxLength: 20
        is_doctor:
          type: boolean
        custom_data: {}
    ReferralSourceWriteRequest:
      type: object
      description: |-
        Write serializer for creating referral sources.

        Practice is set from the URL path by the view.
      properties:
        first_name:
          type: string
          maxLength: 100
        last_name:
          type: string
          maxLength: 100
        specialty:
          type: string
          maxLength: 100
        phone:
          type: string
          maxLength: 20
        email:
          type: string
          format: email
          maxLength: 254
        npi:
          type: string
          maxLength: 20
        is_doctor:
          type: boolean
        custom_data: {}
    RelationshipToSubscriberEnum:
      enum:
        - self
        - spouse
        - child
        - other
      type: string
      description: |-
        * `self` - Self
        * `spouse` - Spouse
        * `child` - Child
        * `other` - Other
    ResolvedScheduleResponse:
      type: object
      properties:
        date:
          type: string
        date_from:
          type: string
        date_to:
          type: string
        blocks:
          type: array
          items:
            type: object
            additionalProperties: {}
        schedule:
          type: object
          additionalProperties:
            type: array
            items:
              type: object
              additionalProperties: {}
    RightOfAccessAccepted:
      type: object
      description: Response payload for the 202 POST response.
      properties:
        report_uid:
          type: string
          format: uuid
        status:
          type: string
        poll_url:
          type: string
      required:
        - poll_url
        - report_uid
        - status
    RightOfAccessRequestRequest:
      type: object
      description: |-
        Validate the POST body for report generation requests.

        ``patient_uid`` is resolved against the URL-scoped practice, not just
        by raw UUID — cross-practice lookups surface as 404.
      properties:
        patient_uid:
          type: string
          format: uuid
        date_from:
          type: string
          format: date
        date_to:
          type: string
          format: date
        format:
          allOf:
            - $ref: '#/components/schemas/FormatEnum'
          default: both
      required:
        - patient_uid
    RightOfAccessStatus:
      type: object
      description: Response payload for the poll endpoint.
      properties:
        report_uid:
          type: string
          format: uuid
        status:
          type: string
        requested_at:
          type: string
          format: date-time
          nullable: true
        ready_at:
          type: string
          format: date-time
          nullable: true
        date_from:
          type: string
          format: date
        date_to:
          type: string
          format: date
        event_count:
          type: integer
        download_urls:
          type: object
          additionalProperties:
            type: string
            nullable: true
        error_message:
          type: string
      required:
        - date_from
        - date_to
        - download_urls
        - error_message
        - event_count
        - ready_at
        - report_uid
        - requested_at
        - status
    RouteEnum:
      enum:
        - infiltration
        - block
        - topical
        - inhalation
      type: string
      description: |-
        * `infiltration` - Infiltration
        * `block` - Block
        * `topical` - Topical
        * `inhalation` - Inhalation
    RouteSlipInsurance:
      type: object
      properties:
        primary_carrier:
          type: string
        primary_plan:
          type: string
        member_id:
          type: string
        group_id:
          type: string
      required:
        - group_id
        - member_id
        - primary_carrier
        - primary_plan
    RouteSlipOperatory:
      type: object
      properties:
        name:
          type: string
        abbreviation:
          type: string
      required:
        - abbreviation
        - name
    RouteSlipPatient:
      type: object
      properties:
        uid:
          type: string
        name:
          type: string
        dob:
          type: string
          format: date
          nullable: true
        chart_number:
          type: string
        phone:
          type: string
        medical_alerts:
          type: array
          items:
            type: string
      required:
        - chart_number
        - dob
        - medical_alerts
        - name
        - phone
        - uid
    RouteSlipProcedure:
      type: object
      properties:
        cdt_code:
          type: string
        description:
          type: string
        fee:
          type: string
        tooth:
          type: string
      required:
        - cdt_code
        - description
        - fee
        - tooth
    RouteSlipProvider:
      type: object
      properties:
        name:
          type: string
        initials:
          type: string
      required:
        - initials
        - name
    RouteSlipResponse:
      type: object
      properties:
        appointment:
          $ref: '#/components/schemas/Appointment'
        patient:
          allOf:
            - $ref: '#/components/schemas/RouteSlipPatient'
          nullable: true
        procedures:
          type: array
          items:
            $ref: '#/components/schemas/RouteSlipProcedure'
        insurance:
          allOf:
            - $ref: '#/components/schemas/RouteSlipInsurance'
          nullable: true
        provider:
          allOf:
            - $ref: '#/components/schemas/RouteSlipProvider'
          nullable: true
        operatory:
          allOf:
            - $ref: '#/components/schemas/RouteSlipOperatory'
          nullable: true
      required:
        - appointment
        - insurance
        - operatory
        - patient
        - procedures
        - provider
    SMSConsent:
      type: object
      description: |-
        Read serializer for the per-(practice, patient) consent row (api#637).

        Exposes the CTIA / 10DLC consent state machine to the staff UI:
        state, opt-out audit columns, first-disclosure stamp, and a
        derived ``last_change_at`` so the "last updated" pill renders
        cleanly whether the row is OPTED_IN (use ``modified``) or
        OPTED_OUT (use ``opted_out_at``).  All fields are read-only --
        mutations flow through the dedicated action endpoints (currently
        just ``force-opt-in``; staff-side STOP follow-up lands later).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        practice:
          type: string
          format: uuid
          readOnly: true
        state:
          allOf:
            - $ref: '#/components/schemas/StateEnum'
          readOnly: true
          description: >-
            Current consent state.  Fresh patients default to OPTED_IN; the row
            is created lazily the first time consent state is queried or
            mutated.


            * `opted_in` - Opted in

            * `opted_out` - Opted out
        opted_out_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When the patient (or staff) opted out.  NULL while OPTED_IN.
        opted_out_reason:
          allOf:
            - $ref: '#/components/schemas/OptedOutReasonEnum'
          readOnly: true
          description: >-
            Why the patient is currently opted out.  Empty while OPTED_IN.  CTIA
            / 10DLC compliance audits want to see the trigger for every opt-out.


            * `patient_request_stop` - Patient texted STOP / UNSUBSCRIBE

            * `staff_action` - Staff opted patient out

            * `error_bounce` - Auto opt-out after delivery failures
        last_help_replied_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: >-
            Last time we sent the HELP / INFO auto-reply to this patient for
            this practice.  Used to throttle to one auto-reply per 24 hours so a
            stuck client repeatedly sending ``HELP`` doesn't spam us into a
            Sinch rate cap.
        first_disclosure_sent_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: >-
            When the CTIA first-message disclosure was prepended to the first
            outbound SMS to this patient from this practice. Set exactly once
            per (practice, patient) pair; later outbounds skip the disclosure
            prefix.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        last_change_at:
          type: string
          format: date-time
          readOnly: true
        last_change_by_id:
          type: string
          format: uuid
          readOnly: true
        review_notes:
          type: string
          readOnly: true
          description: >-
            Free-form context recorded by the staff member at the time of a
            force-opt-in (or future staff-initiated opt-out).  CTIA compliance
            audits want to see why the override was warranted -- typically a
            paraphrase of the patient's verbal request.
      required:
        - created
        - first_disclosure_sent_at
        - id
        - last_change_at
        - last_change_by_id
        - last_help_replied_at
        - modified
        - opted_out_at
        - opted_out_reason
        - patient
        - practice
        - review_notes
        - state
    SMSConsentForceOptInRequest:
      type: object
      description: |-
        Write serializer for the staff force-opt-in action (api#637).

        The body carries a required ``notes`` field -- CTIA / 10DLC permits
        manual re-opt-in only when the patient explicitly asks for it, so
        the operator must record context at the time of the override.
        Empty / whitespace-only notes are refused at the API boundary.
      properties:
        notes:
          type: string
          minLength: 1
          description: >-
            Why the override is warranted -- typically a paraphrase of the
            patient's verbal request.  Required; stored on the consent row's
            ``review_notes`` column for compliance audit.
          maxLength: 1000
      required:
        - notes
    SMSMessage:
      type: object
      description: |-
        Read serializer for the inbox / thread list.

        ``body`` is rendered verbatim except when ``phi_blocked`` is True
        on an inbound row -- the UI should still show the row but hide
        the body behind a "call the patient" banner.  We don't strip the
        body server-side; the field stays on the wire so support tooling
        can audit-read it, and the consumer is expected to honour
        ``phi_blocked`` in the rendering layer.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        practice:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: >-
            Patient this message is associated with.  NULL for inbound messages
            from unknown senders that haven't been matched yet.
        direction:
          allOf:
            - $ref: '#/components/schemas/FaxDirectionEnum'
          readOnly: true
        from_number:
          type: string
          readOnly: true
          description: E.164 sender (may be blank for malformed inbound).
        to_number:
          type: string
          readOnly: true
          description: E.164 destination.
        body:
          type: string
          readOnly: true
          description: >-
            Message body.  Capped at 1600 characters (Sinch's outbound limit per
            send -- segments concatenated for delivery).  PHI is rejected at the
            API boundary; see ``phi_blocked`` for inbound rows whose body
            tripped a PHI pattern.
        sinch_message_id:
          type: string
          readOnly: true
          nullable: true
          description: >-
            Sinch's id for the message -- set once the carrier has accepted it
            (outbound) or notified us (inbound).  NULL for outbound rows still
            queued; unique so a webhook retry can't create a duplicate.
        status:
          allOf:
            - $ref: '#/components/schemas/SMSMessageStatusEnum'
          readOnly: true
        error_message:
          type: string
          readOnly: true
          description: Operator-facing failure reason -- never includes upstream PHI.
        phi_blocked:
          type: boolean
          readOnly: true
          description: >-
            Inbound only: ``True`` when the body matched a PHI pattern.  The
            body is still persisted (it's already on Sinch's side and throwing
            it away would lose the audit trail) but the UI can hide it behind a
            'call the patient' banner.
        sent_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When Sinch accepted the outbound message (status SENT).
        delivered_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When the carrier confirmed handset delivery.
        received_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When the inbound message landed at the webhook.
        sent_by:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff member who initiated an outbound message.  NULL for inbound.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - body
        - created
        - delivered_at
        - direction
        - error_message
        - from_number
        - id
        - modified
        - patient
        - phi_blocked
        - practice
        - received_at
        - sent_at
        - sent_by
        - sinch_message_id
        - status
        - to_number
    SMSMessageCreate:
      type: object
      description: |-
        Write serializer for outbound SMS submission.

        The view sets ``practice``, ``direction``, ``from_number``, and
        ``sent_by`` server-side; the body only needs ``patient`` (or
        ``to_number``) and ``body``.  When ``patient`` is supplied we
        resolve ``to_number`` from the patient's E.164 phone field at
        the view layer; when ``to_number`` is supplied directly we use
        it as-is for unknown-recipient sends (rare but legitimate, e.g.
        a one-off referral text).
      properties:
        patient:
          type: string
          format: uuid
          nullable: true
          description: >-
            Patient this message is for.  When supplied, ``to_number`` is
            resolved from the patient's E.164 phone field server-side.
        to_number:
          type: string
          description: >-
            E.164 destination override.  Required when ``patient`` is omitted;
            ignored otherwise (the patient's phone wins).
          maxLength: 32
        body:
          type: string
          description: >-
            Message body.  Refuses PHI -- diagnosis, prescriptions, tooth-level
            detail, etc.  Send patients into the portal for clinical content.
          maxLength: 1600
      required:
        - body
    SMSMessageCreateRequest:
      type: object
      description: |-
        Write serializer for outbound SMS submission.

        The view sets ``practice``, ``direction``, ``from_number``, and
        ``sent_by`` server-side; the body only needs ``patient`` (or
        ``to_number``) and ``body``.  When ``patient`` is supplied we
        resolve ``to_number`` from the patient's E.164 phone field at
        the view layer; when ``to_number`` is supplied directly we use
        it as-is for unknown-recipient sends (rare but legitimate, e.g.
        a one-off referral text).
      properties:
        patient:
          type: string
          format: uuid
          nullable: true
          description: >-
            Patient this message is for.  When supplied, ``to_number`` is
            resolved from the patient's E.164 phone field server-side.
        to_number:
          type: string
          description: >-
            E.164 destination override.  Required when ``patient`` is omitted;
            ignored otherwise (the patient's phone wins).
          maxLength: 32
        body:
          type: string
          minLength: 1
          description: >-
            Message body.  Refuses PHI -- diagnosis, prescriptions, tooth-level
            detail, etc.  Send patients into the portal for clinical content.
          maxLength: 1600
      required:
        - body
    SMSMessageStatusEnum:
      enum:
        - queued
        - sending
        - sent
        - received
        - delivered
        - failed
      type: string
      description: |-
        * `queued` - Queued
        * `sending` - Sending
        * `sent` - Sent
        * `received` - Received
        * `delivered` - Delivered
        * `failed` - Failed
    SMSReadReceipt:
      type: object
      description: |-
        Read serializer for :class:`SMSReadReceipt` (api#643).

        The list endpoint returns ``[{patient_id, last_read_at}]`` -- the
        rest of the row (practice, user, internal timestamps) is implied
        by the request scope and would just bloat the response.  The
        frontend joins these rows by ``patient_id`` to compute per-patient
        unread counts client-side.
      properties:
        patient_id:
          type: string
          format: uuid
          readOnly: true
        last_read_at:
          type: string
          format: date-time
          readOnly: true
          description: >-
            High-water mark: every inbound message with ``received_at <=
            last_read_at`` is considered read by this user for this patient at
            this practice.  Monotonic -- a would-be earlier write is a no-op.
      required:
        - last_read_at
        - patient_id
    SMSReadReceiptUpsertRequest:
      type: object
      description: |-
        Write serializer for the read-receipt upsert (api#643).

        The view supplies ``practice`` + ``user`` server-side; the body
        only needs the patient and the high-water-mark timestamp.  The
        patient field is validated for practice membership at the view
        layer (the practice context isn't always available on the
        serializer in unit tests).
      properties:
        patient_id:
          type: string
          format: uuid
          description: Patient whose thread the user just finished reading.
        last_read_at:
          type: string
          format: date-time
          description: >-
            ISO-8601 UTC datetime of the most-recently-read message.  Must
            include a timezone suffix ('Z' or '+HH:MM').  The server applies 'no
            earlier than' semantics -- a write older than the current row is
            silently accepted as a no-op.
      required:
        - last_read_at
        - patient_id
    ScheduleOverride:
      type: object
      description: 'GH-711: ``provider`` / ``operatory`` FKs must belong to URL practice.'
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        provider:
          type: string
          format: uuid
          nullable: true
        provider_name:
          type: string
          readOnly: true
        schedule_date:
          type: string
          format: date
        start_time:
          type: string
          format: time
        end_time:
          type: string
          format: time
        schedule_type:
          $ref: '#/components/schemas/ScheduleTypeEnum'
        blockout_type:
          type: string
          description: e.g. lunch, meeting, holiday, time_off
          maxLength: 50
        operatory:
          type: string
          format: uuid
          nullable: true
        status:
          $ref: '#/components/schemas/ScheduleOverrideStatusEnum'
        note:
          type: string
        custom_data: {}
      required:
        - end_time
        - id
        - provider_name
        - schedule_date
        - schedule_type
        - start_time
    ScheduleOverrideRequest:
      type: object
      description: 'GH-711: ``provider`` / ``operatory`` FKs must belong to URL practice.'
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        schedule_date:
          type: string
          format: date
        start_time:
          type: string
          format: time
        end_time:
          type: string
          format: time
        schedule_type:
          $ref: '#/components/schemas/ScheduleTypeEnum'
        blockout_type:
          type: string
          description: e.g. lunch, meeting, holiday, time_off
          maxLength: 50
        operatory:
          type: string
          format: uuid
          nullable: true
        status:
          $ref: '#/components/schemas/ScheduleOverrideStatusEnum'
        note:
          type: string
        custom_data: {}
      required:
        - end_time
        - schedule_date
        - schedule_type
        - start_time
    ScheduleOverrideStatusEnum:
      enum:
        - open
        - closed
        - holiday
      type: string
      description: |-
        * `open` - Open
        * `closed` - Closed
        * `holiday` - Holiday
    ScheduleStatsResponse:
      type: object
      properties:
        total_appointments:
          type: integer
        completed:
          type: integer
        scheduled:
          type: integer
        today:
          type: integer
        this_week:
          type: integer
      required:
        - completed
        - scheduled
        - this_week
        - today
        - total_appointments
    ScheduleTemplate:
      type: object
      description: 'GH-711: ``provider`` / ``operatory`` FKs must belong to URL practice.'
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        provider:
          type: string
          format: uuid
          nullable: true
        provider_name:
          type: string
          readOnly: true
        day_of_week:
          type: integer
          maximum: 2147483647
          minimum: -2147483648
          description: 0=Sunday, 1=Monday, ..., 6=Saturday
        start_time:
          type: string
          format: time
        end_time:
          type: string
          format: time
        schedule_type:
          $ref: '#/components/schemas/ScheduleTypeEnum'
        blockout_type:
          type: string
          description: e.g. lunch, meeting, admin
          maxLength: 50
        operatory:
          type: string
          format: uuid
          nullable: true
        is_active:
          type: boolean
        custom_data: {}
      required:
        - day_of_week
        - end_time
        - id
        - provider_name
        - schedule_type
        - start_time
    ScheduleTemplateRequest:
      type: object
      description: 'GH-711: ``provider`` / ``operatory`` FKs must belong to URL practice.'
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        day_of_week:
          type: integer
          maximum: 2147483647
          minimum: -2147483648
          description: 0=Sunday, 1=Monday, ..., 6=Saturday
        start_time:
          type: string
          format: time
        end_time:
          type: string
          format: time
        schedule_type:
          $ref: '#/components/schemas/ScheduleTypeEnum'
        blockout_type:
          type: string
          description: e.g. lunch, meeting, admin
          maxLength: 50
        operatory:
          type: string
          format: uuid
          nullable: true
        is_active:
          type: boolean
        custom_data: {}
      required:
        - day_of_week
        - end_time
        - schedule_type
        - start_time
    ScheduleTypeEnum:
      enum:
        - office_hours
        - provider
        - blockout
      type: string
      description: |-
        * `office_hours` - Office Hours
        * `provider` - Provider Availability
        * `blockout` - Blockout
    ScopeEnum:
      enum:
        - client
        - practice
        - user
        - app
      type: string
      description: |-
        * `client` - Per-client
        * `practice` - Per-practice
        * `user` - Per-user
        * `app` - Per-3rd-party-app
    ServiceTypeEnum:
      enum:
        - diagnostic
        - preventive
        - basic
        - major
        - endodontics
        - periodontics
        - oral_surgery
        - orthodontics
        - prosthodontics
        - adjunctive
        - other
      type: string
      description: |-
        * `diagnostic` - Diagnostic
        * `preventive` - Preventive
        * `basic` - Basic Restorative
        * `major` - Major Restorative
        * `endodontics` - Endodontics
        * `periodontics` - Periodontics
        * `oral_surgery` - Oral Surgery
        * `orthodontics` - Orthodontics
        * `prosthodontics` - Prosthodontics
        * `adjunctive` - Adjunctive General
        * `other` - Other
    SetupIntentResponse:
      type: object
      description: Response shape for a successful SetupIntent creation.
      properties:
        client_secret:
          type: string
          description: Pass to Stripe.js to confirm the card client-side.
        setup_intent_id:
          type: string
          description: Stripe SetupIntent id (``seti_...``).
        customer_id:
          type: string
          description: Stripe Customer id the card will be attached to.
      required:
        - client_secret
        - customer_id
        - setup_intent_id
    SeverityEnum:
      enum:
        - info
        - warning
        - urgent
      type: string
      description: |-
        * `info` - Info
        * `warning` - Warning
        * `urgent` - Urgent
    ShareCreateInput:
      type: object
      description: |-
        Request body for ``POST /api/v1/practice/<uid>/share/``.

        * ``patient_uid`` -- patient in the practice whose data is being
          shared.  Practice ownership is checked in the view.
        * ``items[]`` -- one or more scope rows.  Empty list is a 400; at
          least one item must be selected.
        * ``recipient_kind`` -- audience classification; drives §7 signature
          requirement.  Must be one of the
          :class:`apps.sharing.models.RecipientKind` values.
        * ``recipient_label`` -- free-form display label (optional).
        * ``purpose`` -- staff-authored short description (required to keep
          the audit trail meaningful; empty strings rejected).
      properties:
        patient_uid:
          type: string
          format: uuid
        items:
          type: array
          items:
            $ref: '#/components/schemas/ShareItemInput'
        recipient_kind:
          $ref: '#/components/schemas/RecipientKindEnum'
        recipient_label:
          type: string
          default: ''
          maxLength: 255
        purpose:
          type: string
          maxLength: 2000
      required:
        - items
        - patient_uid
        - purpose
        - recipient_kind
    ShareCreateInputRequest:
      type: object
      description: |-
        Request body for ``POST /api/v1/practice/<uid>/share/``.

        * ``patient_uid`` -- patient in the practice whose data is being
          shared.  Practice ownership is checked in the view.
        * ``items[]`` -- one or more scope rows.  Empty list is a 400; at
          least one item must be selected.
        * ``recipient_kind`` -- audience classification; drives §7 signature
          requirement.  Must be one of the
          :class:`apps.sharing.models.RecipientKind` values.
        * ``recipient_label`` -- free-form display label (optional).
        * ``purpose`` -- staff-authored short description (required to keep
          the audit trail meaningful; empty strings rejected).
      properties:
        patient_uid:
          type: string
          format: uuid
        items:
          type: array
          items:
            $ref: '#/components/schemas/ShareItemInputRequest'
        recipient_kind:
          $ref: '#/components/schemas/RecipientKindEnum'
        recipient_label:
          type: string
          default: ''
          maxLength: 255
        purpose:
          type: string
          minLength: 1
          maxLength: 2000
      required:
        - items
        - patient_uid
        - purpose
        - recipient_kind
    ShareDetail:
      type: object
      description: |-
        Detail serializer -- base fields, resolved items, and access events.

        Does NOT include ``patient_initials`` -- that handle is list-only
        (api#231); detail views already carry the patient UID and resolve
        to the full chart on the client side.

        Embeds the access-event timeline (api#240) under ``events`` so the
        share-detail-drawer (web#200) can render the audit trail without a
        second round trip.  The list serializer intentionally stays cheap:
        rendering dozens of rows per share on the My Shares table would
        balloon response size for a detail panel that's only open on one
        share at a time.  Events are ordered ascending by ``created_at`` so
        the UI reads as a chronological timeline (oldest first).

        Per api#313, item rows carry a server-resolved ``title`` so the
        drawer doesn't fan out one-fetch-per-item to discover what each
        UUID is.  :meth:`to_representation` bulk-resolves the titles up
        front (one SELECT per resource type) and stashes the map in the
        serializer context so the nested :class:`ShareItemOutputSerializer`
        can render them with a dict lookup.
      properties:
        share_uid:
          type: string
          format: uuid
          readOnly: true
        patient_uid:
          type: string
          format: uuid
          readOnly: true
        status:
          type: string
          readOnly: true
        recipient_kind:
          type: string
          readOnly: true
        recipient_label:
          type: string
          readOnly: true
        purpose:
          type: string
          readOnly: true
        token_preview:
          type: string
          readOnly: true
        created_at:
          type: string
          format: date-time
          readOnly: true
        first_viewed_at:
          type: string
          format: date-time
          readOnly: true
        download_count:
          type: integer
          readOnly: true
        expires_at_unviewed:
          type: string
          format: date-time
          readOnly: true
        expires_at_post_view:
          type: string
          format: date-time
          readOnly: true
        revoked_at:
          type: string
          format: date-time
          readOnly: true
        revoke_reason:
          type: string
          readOnly: true
        created_by:
          type: object
          additionalProperties: {}
          nullable: true
          description: >-
            Return ``{uid, display_name, email}`` for the share creator, or
            ``null``.


            Requires ``select_related("created_by")`` on the queryset to avoid

            per-row N+1 queries.  Views that use this serializer must ensure the

            prefetch is set up (see :class:`ShareListCreateView.get_queryset`
            and

            :class:`ShareDetailView.get_queryset`).
          readOnly: true
        items:
          type: array
          items:
            $ref: '#/components/schemas/ShareItemOutput'
          readOnly: true
        events:
          type: array
          items:
            type: object
            additionalProperties: {}
          description: |-
            Serialize the share's access-event trail oldest-first.

            The view's :meth:`get_queryset` is expected to prefetch
            ``events`` with ``select_related("actor_user")`` so the actor
            lookup stays O(1) per share.  We re-sort here defensively:
            :class:`ShareAccessEvent.Meta.ordering` is descending, but the
            timeline renders more naturally oldest-first.  Sorting in
            Python (rather than a second query) keeps the prefetch cache
            hot -- event counts per share are bounded to a few dozen, so
            the in-memory sort is cheap.
          readOnly: true
      required:
        - created_at
        - created_by
        - download_count
        - events
        - expires_at_post_view
        - expires_at_unviewed
        - first_viewed_at
        - items
        - patient_uid
        - purpose
        - recipient_kind
        - recipient_label
        - revoke_reason
        - revoked_at
        - share_uid
        - status
        - token_preview
    ShareItemInput:
      type: object
      description: |-
        Incoming ``items[]`` element on the create endpoint.

        ``resource_uid`` is optional -- omit it to share every item of
        ``resource_type`` for the patient (category scope).  Validation of
        resource_type vs. the per-resource / category distinction is deferred
        to :class:`apps.sharing.models.ShareItem` (its choices list enforces
        both legal values).
      properties:
        resource_type:
          type: string
          maxLength: 40
        resource_uid:
          type: string
          format: uuid
          nullable: true
      required:
        - resource_type
    ShareItemInputRequest:
      type: object
      description: |-
        Incoming ``items[]`` element on the create endpoint.

        ``resource_uid`` is optional -- omit it to share every item of
        ``resource_type`` for the patient (category scope).  Validation of
        resource_type vs. the per-resource / category distinction is deferred
        to :class:`apps.sharing.models.ShareItem` (its choices list enforces
        both legal values).
      properties:
        resource_type:
          type: string
          minLength: 1
          maxLength: 40
        resource_uid:
          type: string
          format: uuid
          nullable: true
      required:
        - resource_type
    ShareItemOutput:
      type: object
      description: |-
        Read-only rendering of a :class:`ShareItem` row.

        ``title`` is a non-PHI human label resolved server-side from the
        underlying resource (api#313).  Category-scope rows (``resource_uid``
        null) get a fixed label from :data:`_CATEGORY_TITLES`; per-resource
        rows look themselves up in a context-stashed map populated by
        :meth:`ShareDetailSerializer.to_representation` so the resolution is
        O(1) per item with one bulk query per resource type.  When the
        serializer is used outside the detail flow (no map in context) the
        field gracefully degrades to ``""`` rather than firing N+1 lookups.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        resource_type:
          type: string
          readOnly: true
        resource_uid:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        title:
          type: string
          description: |-
            Return a human-readable label for this share-item row.

            Falls through three cases in order:

            1. Category-scope row (``resource_uid is None``) -- return the
               static label from :data:`_CATEGORY_TITLES`, or ``""`` for an
               unknown category type.
            2. Per-resource row whose title was pre-resolved into the
               serializer context by :meth:`ShareDetailSerializer.to_representation`.
            3. Fallback -- ``""``.  The drawer renders the
               ``resource_type`` + UUID handle when ``title`` is empty, so
               callers that skip the detail flow still get a valid response.
          readOnly: true
      required:
        - id
        - resource_type
        - resource_uid
        - title
    ShareListItem:
      type: object
      description: |-
        Row serializer for ``GET /api/v1/practice/<uid>/share/``.

        Adds ``patient_initials`` on top of the shared base -- a two-letter
        first-initial + last-initial handle (e.g. ``J.D.`` for Jane Doe) the
        My Shares UI scans instead of the raw patient UUID.  Names
        themselves are PHI and stay out of list rows; initials are enough to
        disambiguate without leaking identity on a shared screen.

        Empty / null names collapse to whichever half is non-empty, or
        ``??`` if both are empty -- matching the frontend's own
        ``patientInitials()`` helper so staff see a stable handle regardless
        of which side computes it.

        Deliberately NOT on :class:`ShareDetailSerializer`, the bundle
        manifest, or :class:`ShareSummarySerializer`.  The detail panel
        already knows the patient (it's an audit-side view for staff with
        patient access); the bundle / redemption path is cross-tenant and
        must not carry patient handles at all.
      properties:
        share_uid:
          type: string
          format: uuid
          readOnly: true
        patient_uid:
          type: string
          format: uuid
          readOnly: true
        status:
          type: string
          readOnly: true
        recipient_kind:
          type: string
          readOnly: true
        recipient_label:
          type: string
          readOnly: true
        purpose:
          type: string
          readOnly: true
        token_preview:
          type: string
          readOnly: true
        created_at:
          type: string
          format: date-time
          readOnly: true
        first_viewed_at:
          type: string
          format: date-time
          readOnly: true
        download_count:
          type: integer
          readOnly: true
        expires_at_unviewed:
          type: string
          format: date-time
          readOnly: true
        expires_at_post_view:
          type: string
          format: date-time
          readOnly: true
        revoked_at:
          type: string
          format: date-time
          readOnly: true
        revoke_reason:
          type: string
          readOnly: true
        created_by:
          type: object
          additionalProperties: {}
          nullable: true
          description: >-
            Return ``{uid, display_name, email}`` for the share creator, or
            ``null``.


            Requires ``select_related("created_by")`` on the queryset to avoid

            per-row N+1 queries.  Views that use this serializer must ensure the

            prefetch is set up (see :class:`ShareListCreateView.get_queryset`
            and

            :class:`ShareDetailView.get_queryset`).
          readOnly: true
        patient_initials:
          type: string
          description: |-
            Return the patient's upper-cased first + last initial.

            Falls back to the non-empty half (``J.`` / ``.D.``-style) when
            only one name is populated, or ``??`` when both are empty.
          readOnly: true
      required:
        - created_at
        - created_by
        - download_count
        - expires_at_post_view
        - expires_at_unviewed
        - first_viewed_at
        - patient_initials
        - patient_uid
        - purpose
        - recipient_kind
        - recipient_label
        - revoke_reason
        - revoked_at
        - share_uid
        - status
        - token_preview
    ShareRedeemInput:
      type: object
      description: |-
        Accepts **either** an opaque URL token **or** a Diceware phrase.

        Exactly one of ``token`` / ``phrase`` must be present; supplying both
        (or neither) is a 400.  We keep the two inputs on a single endpoint so
        the recipient UX can offer a single textbox that auto-detects the
        format without round-tripping to pick which endpoint to call.
      properties:
        token:
          type: string
          maxLength: 128
        phrase:
          type: string
          maxLength: 256
    ShareRedeemInputRequest:
      type: object
      description: |-
        Accepts **either** an opaque URL token **or** a Diceware phrase.

        Exactly one of ``token`` / ``phrase`` must be present; supplying both
        (or neither) is a 400.  We keep the two inputs on a single endpoint so
        the recipient UX can offer a single textbox that auto-detects the
        format without round-tripping to pick which endpoint to call.
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 128
        phrase:
          type: string
          minLength: 1
          maxLength: 256
    SignatureCreateRequest:
      type: object
      description: |-
        Validate and persist an inbound signature capture.

        The pad widget posts a multipart body with the rendered PNG
        attached as ``image``; auxiliary fields ride alongside as
        standard form fields.  The serializer accepts a friendly
        ``attached_content_type`` shape (``"app_label.model"``) instead
        of forcing the caller to look up a raw ContentType PK.

        Server-managed fields (``practice``, ``signed_at``, ``ip_address``,
        ``user_agent``, ``content_type``) are not exposed in ``fields``;
        the view populates them and we reject any client-side attempt to
        set them by simply not listing them.
      properties:
        signer_type:
          allOf:
            - $ref: '#/components/schemas/SignerTypeEnum'
          description: |-
            Whether a patient or a staff user signed.

            * `patient` - Patient
            * `staff` - Staff
        signer_patient:
          type: string
          format: uuid
          nullable: true
          description: Patient who signed (set when signer_type='patient').
        signer_user:
          type: string
          format: uuid
          nullable: true
          description: Staff user who signed (set when signer_type='staff').
        signer_name_typed:
          type: string
          minLength: 1
          description: >-
            Full name the signer typed alongside the signature pad (legal
            identity claim).
          maxLength: 255
        image:
          type: string
          format: binary
          description: Rendered signature PNG produced by the signature-pad widget.
        image_mime_type:
          type: string
          minLength: 1
          description: MIME type of the stored signature image.
          maxLength: 64
        attached_content_type:
          type: string
          writeOnly: true
          description: >-
            ContentType of the attached object as "app_label.model" (e.g.
            "portal.formsubmission").
        object_id:
          type: string
          description: >-
            PK of the attached object (generic FK; CharField to accept either
            UUID or int).
          maxLength: 64
        consent_text_snapshot:
          type: string
          minLength: 1
          description: >-
            Literal text the signer agreed to, frozen at signing time
            (non-repudiation).
      required:
        - consent_text_snapshot
        - image
        - signer_name_typed
        - signer_type
    SignatureDetail:
      type: object
      description: |-
        Detail (full) shape for a single signature row.

        All fields are read-only.  The endpoint is immutable by contract:
        no PUT / PATCH / DELETE -- evidence rows survive corrections via
        the audit trail, not in-place edits.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        practice:
          type: string
          format: uuid
          readOnly: true
          description: Practice that owns this signature evidence row.
        signer_type:
          allOf:
            - $ref: '#/components/schemas/SignerTypeEnum'
          readOnly: true
          description: |-
            Whether a patient or a staff user signed.

            * `patient` - Patient
            * `staff` - Staff
        signer_patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Patient who signed (set when signer_type='patient').
        patient_first_name:
          type: string
          readOnly: true
        patient_last_name:
          type: string
          readOnly: true
        signer_user:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff user who signed (set when signer_type='staff').
        signer_email:
          type: string
          format: email
          readOnly: true
        signer_name_typed:
          type: string
          readOnly: true
          description: >-
            Full name the signer typed alongside the signature pad (legal
            identity claim).
        image_url:
          type: string
          nullable: true
          description: |-
            Return a short-lived signed URL for the signature image, or None.

            ``get_presigned_url`` defaults to a 5-minute TTL, which is
            short enough that a leaked URL has minimal residual risk and
            long enough that a slow-rendering signature panel doesn't
            race the expiry.  Local-dev fallback returns the static media
            URL when DO Spaces is not configured (matches the doccenter /
            imaging / feedback behaviour).
          readOnly: true
        image_mime_type:
          type: string
          readOnly: true
          description: MIME type of the stored signature image.
        attached_content_type:
          type: string
          nullable: true
          readOnly: true
        object_id:
          type: string
          readOnly: true
          description: >-
            PK of the attached object (generic FK; CharField to accept either
            UUID or int).
        consent_text_snapshot:
          type: string
          readOnly: true
          description: >-
            Literal text the signer agreed to, frozen at signing time
            (non-repudiation).
        signed_at:
          type: string
          format: date-time
          readOnly: true
          description: >-
            UTC timestamp of the signing event (server-side; cannot be
            back-dated).
        ip_address:
          type: string
          readOnly: true
          description: >-
            Source IP of the signing request (X-Forwarded-For first hop, then
            REMOTE_ADDR).
        user_agent:
          type: string
          readOnly: true
          description: User-Agent string of the signing request.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - attached_content_type
        - consent_text_snapshot
        - created
        - id
        - image_mime_type
        - image_url
        - ip_address
        - modified
        - object_id
        - patient_first_name
        - patient_last_name
        - practice
        - signed_at
        - signer_email
        - signer_name_typed
        - signer_patient
        - signer_type
        - signer_user
        - user_agent
    SignatureList:
      type: object
      description: |-
        Compact list shape for the signature evidence list endpoint.

        The signed image URL is omitted to keep list payloads small
        (signing a URL costs a roundtrip to the storage backend) -- open
        the detail view to fetch a signed URL.  ``signer_email`` /
        ``patient_name`` are denormalised so the list UI doesn't have to
        round-trip through patient / user endpoints for every row.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        signer_type:
          allOf:
            - $ref: '#/components/schemas/SignerTypeEnum'
          readOnly: true
          description: |-
            Whether a patient or a staff user signed.

            * `patient` - Patient
            * `staff` - Staff
        signer_patient:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Patient who signed (set when signer_type='patient').
        patient_first_name:
          type: string
          readOnly: true
        patient_last_name:
          type: string
          readOnly: true
        signer_user:
          type: string
          format: uuid
          readOnly: true
          nullable: true
          description: Staff user who signed (set when signer_type='staff').
        signer_email:
          type: string
          format: email
          readOnly: true
        signer_name_typed:
          type: string
          readOnly: true
          description: >-
            Full name the signer typed alongside the signature pad (legal
            identity claim).
        image_mime_type:
          type: string
          readOnly: true
          description: MIME type of the stored signature image.
        attached_content_type:
          type: string
          nullable: true
          readOnly: true
        object_id:
          type: string
          readOnly: true
          description: >-
            PK of the attached object (generic FK; CharField to accept either
            UUID or int).
        signed_at:
          type: string
          format: date-time
          readOnly: true
          description: >-
            UTC timestamp of the signing event (server-side; cannot be
            back-dated).
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - attached_content_type
        - created
        - id
        - image_mime_type
        - object_id
        - patient_first_name
        - patient_last_name
        - signed_at
        - signer_email
        - signer_name_typed
        - signer_patient
        - signer_type
        - signer_user
    SignerTypeEnum:
      enum:
        - patient
        - staff
      type: string
      description: |-
        * `patient` - Patient
        * `staff` - Staff
    SignupInviteCodeError:
      type: object
      description: >-
        OpenAPI shape for the 400 returned by signup when invite-code validation
        fails.


        DRF renders field errors as ``{"<field>": ["msg1", "msg2"]}`` so the

        error response is a list of strings keyed on the offending field --

        here ``invite_code``.  Documented as its own serializer so the

        OpenAPI schema can advertise the exact key + value type to client

        apps without resorting to a free-form ``object``.
      properties:
        invite_code:
          type: array
          items:
            type: string
          description: One or more human-readable reasons the invite code was rejected.
      required:
        - invite_code
    SignupRequest:
      type: object
      description: >-
        Public self-service signup payload (GH-440).


        Creates a brand-new :class:`apps.practices.models.Client` plus a

        :class:`User` and a :class:`ClientMembership` with role

        :class:`ClientRole.OWNER` so the new account immediately owns its

        own tenant.  The newly created user has no usable password (passkey

        enrolment is the next step in the signup wizard) -- callers should

        redirect to the magic-link / passkey-registration flow once this

        endpoint returns 201.


        ``invite_code`` is always accepted on the request shape so the

        client doesn't have to know whether the flag is on.  Whether it's

        *required* depends on
        :data:`waffle.flag_is_active("invited_signup_required")`,

        which the view enforces -- when the flag is off, a missing or

        invalid code is silently ignored; when it's on, both omission and

        invalidity surface as 400 errors.
      properties:
        email:
          type: string
          format: email
          minLength: 1
        first_name:
          type: string
          minLength: 1
          maxLength: 150
        last_name:
          type: string
          minLength: 1
          maxLength: 150
        client_name:
          type: string
          minLength: 1
          description: Display name for the new business entity (DSO or solo practitioner).
          maxLength: 255
        invite_code:
          type: string
          description: >-
            Required only when the ``invited_signup_required`` Waffle flag is
            on.  Single-use; consumed atomically on successful signup.
        npi:
          type: string
          description: >-
            10-digit National Provider Identifier.  Required when the
            ``npi_verification_required`` Waffle flag is on.
          pattern: ^\d{10}$
        state:
          type: string
          description: >-
            Two-letter US state code where the practice operates.  Required when
            the ``npi_verification_required`` Waffle flag is on; matched
            case-insensitively against the NPI Registry's address rows.
          maxLength: 2
      required:
        - client_name
        - email
        - first_name
        - last_name
    SignupResponse:
      type: object
      description: |-
        Minimal 201 response body for ``POST /signup/``.

        Returns the new user's UID and email plus the new client's UID --
        enough for the client app to redirect into the passkey-registration
        flow without a follow-up ``/me/`` call.
      properties:
        user_id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        client_id:
          type: string
          format: uuid
      required:
        - client_id
        - email
        - user_id
    Source:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          maxLength: 255
        source_type:
          $ref: '#/components/schemas/SourceTypeEnum'
        is_active:
          type: boolean
        notes:
          type: string
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - created
        - id
        - modified
        - name
        - source_type
    SourceRequest:
      type: object
      description: >-
        Mixin for ModelSerializers that handle ``custom_data`` with per-app
        scoping.


        Include this mixin in any serializer that exposes ``custom_data``.

        It intercepts ``create()`` and ``update()`` to merge the submitted

        dict into the correct ``custom_data[client_id]`` namespace.


        For non-OAuth requests (web app, admin), the submitted dict replaces

        ``custom_data`` entirely (full access).
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        source_type:
          $ref: '#/components/schemas/SourceTypeEnum'
        is_active:
          type: boolean
        notes:
          type: string
        custom_data: {}
      required:
        - name
        - source_type
    SourceTypeEnum:
      enum:
        - webcam
        - sensor
        - file_upload
      type: string
      description: |-
        * `webcam` - Webcam / Intraoral Camera
        * `sensor` - X-Ray Sensor
        * `file_upload` - File Upload
    StageEnum:
      enum:
        - healthy
        - stage_i
        - stage_ii
        - stage_iii
        - stage_iv
      type: string
      description: |-
        * `healthy` - Healthy
        * `stage_i` - Stage I - Initial
        * `stage_ii` - Stage II - Moderate
        * `stage_iii` - Stage III - Severe
        * `stage_iv` - Stage IV - Advanced
    StateEnum:
      enum:
        - opted_in
        - opted_out
      type: string
      description: |-
        * `opted_in` - Opted in
        * `opted_out` - Opted out
    StatePrivacyConfig:
      type: object
      description: Read-only serializer for per-state privacy law configuration.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        state:
          type: string
          readOnly: true
          description: US state abbreviation (e.g. CA, TX)
        state_name:
          type: string
          readOnly: true
          description: Full state name
        config:
          readOnly: true
          description: >-
            Privacy ruleset for this state. Keys: law_name, retention_days,
            opt_out_required, sensitive_data_consent, right_to_delete,
            right_to_access, data_minimization, minor_consent_age,
            breach_notification_hours.
        is_active:
          type: boolean
          readOnly: true
      required:
        - config
        - id
        - is_active
        - state
        - state_name
    Status4d1Enum:
      enum:
        - pending
        - in_progress
        - submitted
        - expired
        - archived
      type: string
      description: |-
        * `pending` - Pending — link sent, not opened
        * `in_progress` - In progress — patient opened, not submitted
        * `submitted` - Submitted
        * `expired` - Expired
        * `archived` - Archived
    Status53dEnum:
      enum:
        - pending
        - ready_for_review
        - approved
        - converted
        - merged
        - rejected
        - dismissed
      type: string
      description: |-
        * `pending` - Pending review
        * `ready_for_review` - Ready for review
        * `approved` - Approved -- new patient created
        * `converted` - Converted to new patient
        * `merged` - Merged into existing patient
        * `rejected` - Rejected
        * `dismissed` - Dismissed
    StripeNotConfigured:
      type: object
      description: |-
        503 envelope returned when Stripe credentials are unset.

        Distinct from a 5xx server error so the frontend can render a clear
        "payment processing is not yet enabled in this environment" message
        without surfacing a stack trace.
      properties:
        status:
          type: string
        message:
          type: string
      required:
        - message
        - status
    StripeStatusEnum:
      enum:
        - PENDING
        - SUCCEEDED
        - FAILED
        - REFUNDED
      type: string
      description: |-
        * `PENDING` - Pending
        * `SUCCEEDED` - Succeeded
        * `FAILED` - Failed
        * `REFUNDED` - Refunded
    SubmittedViaDb2Enum:
      enum:
        - kiosk
        - portal
        - staff_form
      type: string
      description: |-
        * `kiosk` - Kiosk
        * `portal` - Patient Portal
        * `staff_form` - Staff-entered form
    SupportPatient:
      type: object
      description: |-
        PHI-free patient representation for the support tool.

        This serializer is the HIPAA firewall between support techs and
        patient data.  Support agents are cross-practice (not scoped to any
        one office) and MUST NOT see PHI.  The field list is therefore an
        allow-list -- only the fields named in ``Meta.fields`` are ever
        serialized, and every field here has been individually justified
        as non-PHI.

        Exposed fields:

        * ``id`` -- the patient's opaque UUID; used to correlate support
          tickets with a specific record without revealing identity.
        * ``initials`` -- first letter of first name + first letter of last
          name, uppercased.  Enough to confirm "you're looking at the right
          person" in a phone conversation, never enough to identify them.
        * ``practice`` -- {id, name} of the owning practice, so the tech
          knows which office's dataset this patient belongs to.
        * ``status`` -- Patient.status text choice (active / inactive /
          archived / deceased / prospective).  Operational, not clinical.
        * ``last_appointment_at`` -- ISO8601 date of the most recent
          appointment, or null.  Helps determine whether the record is stale.
        * ``balance_outstanding`` -- decimal dollars total balance, or null
          if the practice doesn't track balances.
        * ``created`` / ``modified`` -- record-level timestamps.

        Never serialized (and no way to opt back in): first_name, last_name,
        preferred_name, email, dob, phone_*, address_*, city, state,
        zip_code, gender, insurance IDs, clinical fields, notes.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        initials:
          type: string
          description: |-
            Return uppercased first-initial + last-initial, e.g. "JS".

            Returns an empty string if both names are missing.  This is a
            computed property; the underlying first_name / last_name fields
            are never themselves serialized.
          readOnly: true
        practice:
          allOf:
            - $ref: '#/components/schemas/SupportPracticeMini'
          readOnly: true
        status:
          allOf:
            - $ref: '#/components/schemas/PatientStatusEnum'
          readOnly: true
        last_appointment_at:
          type: string
          nullable: true
          description: >-
            Return the ISO8601 timestamp of the patient's most recent
            appointment.


            Uses the ``last_appointment_at`` annotation added by the view's

            queryset when present; otherwise falls back to a direct query.
          readOnly: true
        balance_outstanding:
          type: string
          nullable: true
          description: |-
            Return the patient's outstanding balance as a stringified decimal.

            Pulled from the related :class:`apps.billing.models.PatientBalance`
            row if one exists, otherwise None.  Returned as a string to avoid
            float rounding in transit -- clients can parse with Decimal.
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - balance_outstanding
        - created
        - id
        - initials
        - last_appointment_at
        - modified
        - practice
        - status
    SupportPracticeMini:
      type: object
      description: |-
        Minimal practice reference embedded in support patient responses.

        Only the id and name are exposed -- enough for a support tech to
        identify the practice without leaking operational detail.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          readOnly: true
      required:
        - id
        - name
    SupportSessionCreateResponse:
      type: object
      description: |-
        Response shape for ``POST /api/v1/support/sessions/``.

        The technician receives:

        * ``id`` / ``status`` -- session identifiers for follow-on calls.
        * ``code`` / ``code_expires_at`` -- to read aloud / paste to the user.
        * ``livekit_url`` / ``livekit_token`` / ``livekit_room`` -- enough
          to dial into the LiveKit SFU as a *subscriber*.
        * ``token_expires_at`` -- the JWT ``exp`` claim, separate from the
          code TTL.  The viewer uses it to refresh ahead of expiry.
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
        code:
          type: string
        code_expires_at:
          type: string
          format: date-time
        livekit_url:
          type: string
        livekit_token:
          type: string
        livekit_room:
          type: string
        token_expires_at:
          type: integer
      required:
        - code
        - code_expires_at
        - id
        - livekit_room
        - livekit_token
        - livekit_url
        - status
        - token_expires_at
    SupportSessionEndResponse:
      type: object
      description: |-
        Response shape for ``POST /sessions/<id>/end/``.

        Returns the terminal state so the caller can confirm the
        transition without a second GET.
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
        ended_at:
          type: string
          format: date-time
      required:
        - ended_at
        - id
        - status
    SupportSessionList:
      type: object
      description: |-
        Compact representation for the audit list endpoint.

        Returns the session metadata (who, what, when) without exposing
        the LiveKit tokens or the unredeemed code.  The unredeemed code
        is omitted on purpose -- a stale list view in a tech's browser
        must never be a way to harvest still-valid codes.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        tech_email:
          type: string
          nullable: true
          readOnly: true
        user_email:
          type: string
          nullable: true
          readOnly: true
        practice_id:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        status:
          allOf:
            - $ref: '#/components/schemas/SupportSessionListStatusEnum'
          readOnly: true
          description: |-
            Current lifecycle state.

            * `pending` - Pending
            * `active` - Active
            * `ended` - Ended
            * `expired` - Expired
        code_expires_at:
          type: string
          format: date-time
          readOnly: true
          description: UTC timestamp the code stops being redeemable.
        redeemed_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: UTC timestamp the code was redeemed.
        ended_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: UTC timestamp the session was ended.
        livekit_room_id:
          type: string
          readOnly: true
          description: LiveKit room identifier this session was scoped to.
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
      required:
        - code_expires_at
        - created
        - ended_at
        - id
        - livekit_room_id
        - practice_id
        - redeemed_at
        - status
        - tech_email
        - user_email
    SupportSessionListStatusEnum:
      enum:
        - pending
        - active
        - ended
        - expired
      type: string
      description: |-
        * `pending` - Pending
        * `active` - Active
        * `ended` - Ended
        * `expired` - Expired
    SupportSessionRedeemRequestRequest:
      type: object
      description: |-
        Request body for ``POST /api/v1/support/sessions/redeem/``.

        Only the code is accepted.  Practice context is *not* taken from
        the body -- if the caller has a current practice membership we
        record it server-side from the session, never from a
        client-supplied UUID, to prevent a malicious user from
        fabricating a practice attribution on a redemption row.
      properties:
        code:
          type: string
          minLength: 1
          maxLength: 32
      required:
        - code
    SupportSessionRedeemResponse:
      type: object
      description: |-
        Response shape for ``POST /api/v1/support/sessions/redeem/``.

        The redeeming user receives a *publisher* token + the matching
        LiveKit room.  The session id is included so the user-side UI can
        POST to ``/end/`` when the customer wants to disconnect.
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
        livekit_url:
          type: string
        livekit_token:
          type: string
        livekit_room:
          type: string
        token_expires_at:
          type: integer
      required:
        - id
        - livekit_room
        - livekit_token
        - livekit_url
        - status
        - token_expires_at
    SyncStatusResponse:
      type: object
      description: Per-entity sync status row returned by the sync-status endpoints.
      properties:
        entity_type:
          type: string
        last_synced_at:
          type: string
          nullable: true
        last_record_count:
          type: integer
        is_running:
          type: boolean
        error_message:
          type: string
      required:
        - entity_type
        - error_message
        - is_running
        - last_record_count
        - last_synced_at
    SyncStatusUpdateRequestRequest:
      type: object
      description: Payload for ``POST /sync/status/<entity_type>/``.
      properties:
        last_synced_at:
          type: string
          format: date-time
          nullable: true
        last_record_count:
          type: integer
          default: 0
        is_running:
          type: boolean
          default: false
        error_message:
          type: string
          default: ''
    Tablet:
      type: object
      description: |-
        Read serializer for the staff list / detail surface.

        Derives ``is_stale`` at read time from the row rather than
        persisting the bit -- the stale check is pure clock arithmetic
        against ``last_seen_at`` and persisting would create the same
        "stored bucket decays daily" problem Aaron flagged for aged
        balances.  Same logic applies for ``is_revoked``: it is just
        ``revoked_at IS NOT NULL`` and rendering it as an explicit bool
        keeps the API ergonomic.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          readOnly: true
          description: >-
            Staff-assigned label for the tablet, shown in the audit log and the
            settings UI.
        paired_at:
          type: string
          format: date-time
          readOnly: true
          description: When the tablet completed the pair-completion exchange.
        last_seen_at:
          type: string
          format: date-time
          readOnly: true
          description: >-
            Most recent authenticated request from this tablet (bumped per
            request).
        revoked_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: >-
            Set when staff revokes the tablet via the settings UI; null while
            the tablet is active.
        is_stale:
          type: boolean
          readOnly: true
        is_revoked:
          type: boolean
          readOnly: true
        paired_by_email:
          type: string
          nullable: true
          readOnly: true
        revoked_by_email:
          type: string
          nullable: true
          readOnly: true
      required:
        - id
        - is_revoked
        - is_stale
        - last_seen_at
        - name
        - paired_at
        - paired_by_email
        - revoked_at
        - revoked_by_email
    TabletRenameRequest:
      type: object
      description: |-
        Validate rename payloads.

        The cross-row uniqueness check lives in the view (it needs the
        Tablet instance + the practice from the URL) -- the serializer
        only enforces shape.
      properties:
        name:
          type: string
          minLength: 1
          description: New staff-assigned label.
          maxLength: 64
      required:
        - name
    TabletSendRequest:
      type: object
      description: |-
        Validate the staff-side ``send-to-tablet`` payload (api#684 / api#771).

        Shape check only -- the action vocabulary is enforced in the view
        against :data:`apps.tablets.actions.TABLET_ACTIONS` so an unknown
        action returns a 400 with the same shape DRF uses for serializer
        errors.  We keep the action vocab out of the serializer so adding
        a new action does not couple to the serializer module.

        ``payload`` is a free-form dict -- per-action payload validation
        lives in the view (and will likely move into a dispatch table when
        we add another action).  Caps the dict size at 4 KiB serialized to
        keep an obviously-abusive caller from ballooning the channel layer.

        ``form_session`` is a sibling (not nested under ``payload``) so the
        api#771 contract reads cleanly at the JSON level:
        ``{action: form_session, form_session: {bundle_id, ...}}``.  When
        ``action == "form_session"`` the block is required; for the other
        verbs it is ignored.  Keeping it sibling-shaped also means the
        audit emitter can introspect the block without unwrapping a
        free-form ``payload`` dict.
      properties:
        action:
          type: string
          minLength: 1
          description: Action verb the tablet should react to (e.g. open_patient_intake).
          maxLength: 64
        payload:
          type: object
          additionalProperties: {}
          description: Per-action payload; shape validated in the view.
        form_session:
          allOf:
            - $ref: '#/components/schemas/FormSessionSpecRequest'
          description: >-
            Form-session spec -- REQUIRED when action == 'form_session', ignored
            otherwise.  See api#771.
      required:
        - action
    ThrottleModeEnum:
      enum:
        - default
        - audit
        - enforce
      type: string
      description: |-
        * `default` - Default (inherit global)
        * `audit` - Audit
        * `enforce` - Enforce
    TimePeriodEnum:
      enum:
        - calendar_year
        - service_year
        - lifetime
        - per_occurrence
      type: string
      description: |-
        * `calendar_year` - Calendar Year
        * `service_year` - Service/Benefit Year
        * `lifetime` - Lifetime
        * `per_occurrence` - Per Occurrence
    TokenVerifyRequest:
      type: object
      properties:
        token:
          type: string
          writeOnly: true
          minLength: 1
      required:
        - token
    ToothCondition:
      type: object
      description: |-
        Full CRUD serializer for tooth conditions (odontogram).

        The ``patient`` field is read-only because it is set from the URL path.
        GH-711: bulk-injected ``patient`` is practice-validated.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        tooth_num:
          type: string
          description: 'Universal numbering: 1-32 (permanent), A-T (primary)'
          maxLength: 5
        condition:
          $ref: '#/components/schemas/ConditionEnum'
        dentition:
          $ref: '#/components/schemas/DentitionEnum'
        surfaces:
          type: string
          description: 'Affected surfaces: M, O, D, B/F, L, I (e.g. ''MOD'')'
          maxLength: 10
        material:
          type: string
          description: e.g. PFM, zirconia, amalgam, composite
          maxLength: 50
        note:
          type: string
        date_identified:
          type: string
          format: date
          nullable: true
        is_current:
          type: boolean
          description: False = historical condition superseded by a newer one
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - condition
        - created
        - id
        - modified
        - patient
        - tooth_num
    ToothConditionRequest:
      type: object
      description: |-
        Full CRUD serializer for tooth conditions (odontogram).

        The ``patient`` field is read-only because it is set from the URL path.
        GH-711: bulk-injected ``patient`` is practice-validated.
      properties:
        tooth_num:
          type: string
          minLength: 1
          description: 'Universal numbering: 1-32 (permanent), A-T (primary)'
          maxLength: 5
        condition:
          $ref: '#/components/schemas/ConditionEnum'
        dentition:
          $ref: '#/components/schemas/DentitionEnum'
        surfaces:
          type: string
          description: 'Affected surfaces: M, O, D, B/F, L, I (e.g. ''MOD'')'
          maxLength: 10
        material:
          type: string
          description: e.g. PFM, zirconia, amalgam, composite
          maxLength: 50
        note:
          type: string
        date_identified:
          type: string
          format: date
          nullable: true
        is_current:
          type: boolean
          description: False = historical condition superseded by a newer one
        custom_data: {}
      required:
        - condition
        - tooth_num
    TopicEnum:
      enum:
        - oral_hygiene
        - brushing
        - flossing
        - interproximal
        - electric_toothbrush
        - fluoride
        - diet_counseling
        - tobacco_cessation
        - perio_disease
        - caries_risk
        - dry_mouth
        - bruxism
        - sensitivity
        - whitening
        - ortho_care
        - implant_care
        - post_op
        - pre_med
        - other
      type: string
      description: |-
        * `oral_hygiene` - Oral Hygiene Instructions
        * `brushing` - Brushing Technique
        * `flossing` - Flossing Technique
        * `interproximal` - Interproximal Cleaning
        * `electric_toothbrush` - Electric Toothbrush
        * `fluoride` - Fluoride Use
        * `diet_counseling` - Diet Counseling
        * `tobacco_cessation` - Tobacco Cessation
        * `perio_disease` - Periodontal Disease
        * `caries_risk` - Caries Risk
        * `dry_mouth` - Dry Mouth / Xerostomia
        * `bruxism` - Bruxism / Night Guard
        * `sensitivity` - Sensitivity
        * `whitening` - Whitening Options
        * `ortho_care` - Orthodontic Care
        * `implant_care` - Implant Care
        * `post_op` - Post-Operative Instructions
        * `pre_med` - Premedication Reminder
        * `other` - Other
    TreatmentPlan:
      type: object
      description: Read serializer for treatment plans with nested items.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        provider:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        provider_name:
          type: string
          nullable: true
          readOnly: true
        name:
          type: string
          readOnly: true
        status:
          allOf:
            - $ref: '#/components/schemas/TreatmentPlanStatusEnum'
          readOnly: true
        date_created:
          type: string
          format: date
          readOnly: true
        date_accepted:
          type: string
          format: date
          readOnly: true
          nullable: true
        note:
          type: string
          readOnly: true
        items:
          type: array
          items:
            $ref: '#/components/schemas/TreatmentPlanItem'
          readOnly: true
        item_count:
          type: integer
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - created
        - date_accepted
        - date_created
        - id
        - item_count
        - items
        - modified
        - name
        - note
        - patient
        - provider
        - provider_name
        - status
    TreatmentPlanItem:
      type: object
      description: >-
        Full CRUD serializer for treatment plan items.


        The ``patient`` and ``practice`` fields are read-only because they are
        set

        from the URL path by the view's ``perform_create`` method.


        Exposes ``procedure_code_fee`` (denormalized from the linked
        ProcedureCode's

        ``default_fee``) so the UI can show the practice's UCR fee even when the

        item's own ``planned_fee`` is 0 (e.g. fresh imports that haven't run

        through the estimate flow yet).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        provider:
          type: string
          format: uuid
          nullable: true
        treatment_plan:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        procedure_code:
          type: string
          format: uuid
          nullable: true
        proc_code:
          type: string
          maxLength: 20
        procedure_code_fee:
          type: string
          readOnly: true
        description:
          type: string
          maxLength: 255
        tooth_num:
          type: string
          maxLength: 10
        surface:
          type: string
          maxLength: 20
        planned_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        date_planned:
          type: string
          format: date
          nullable: true
        status:
          type: string
          description: planned, completed
          maxLength: 30
        est_primary_ins:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        est_secondary_ins:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
          readOnly: true
        plan_name:
          type: string
          maxLength: 255
        priority:
          type: integer
          maximum: 2147483647
          minimum: 0
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - created
        - est_primary_ins
        - est_secondary_ins
        - id
        - modified
        - patient
        - procedure_code_fee
        - treatment_plan
    TreatmentPlanItemRequest:
      type: object
      description: >-
        Full CRUD serializer for treatment plan items.


        The ``patient`` and ``practice`` fields are read-only because they are
        set

        from the URL path by the view's ``perform_create`` method.


        Exposes ``procedure_code_fee`` (denormalized from the linked
        ProcedureCode's

        ``default_fee``) so the UI can show the practice's UCR fee even when the

        item's own ``planned_fee`` is 0 (e.g. fresh imports that haven't run

        through the estimate flow yet).
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        procedure_code:
          type: string
          format: uuid
          nullable: true
        proc_code:
          type: string
          maxLength: 20
        description:
          type: string
          maxLength: 255
        tooth_num:
          type: string
          maxLength: 10
        surface:
          type: string
          maxLength: 20
        planned_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        date_planned:
          type: string
          format: date
          nullable: true
        status:
          type: string
          description: planned, completed
          maxLength: 30
        plan_name:
          type: string
          maxLength: 255
        priority:
          type: integer
          maximum: 2147483647
          minimum: 0
        custom_data: {}
    TreatmentPlanItemWrite:
      type: object
      description: Write serializer for treatment plan items used in nested creation.
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        procedure_code:
          type: string
          format: uuid
          nullable: true
        proc_code:
          type: string
          maxLength: 20
        description:
          type: string
          maxLength: 255
        tooth_num:
          type: string
          maxLength: 10
        surface:
          type: string
          maxLength: 20
        planned_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        date_planned:
          type: string
          format: date
          nullable: true
        status:
          type: string
          description: planned, completed
          maxLength: 30
        est_primary_ins:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        est_secondary_ins:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        plan_name:
          type: string
          maxLength: 255
        priority:
          type: integer
          maximum: 2147483647
          minimum: 0
        custom_data: {}
    TreatmentPlanItemWriteRequest:
      type: object
      description: Write serializer for treatment plan items used in nested creation.
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        procedure_code:
          type: string
          format: uuid
          nullable: true
        proc_code:
          type: string
          maxLength: 20
        description:
          type: string
          maxLength: 255
        tooth_num:
          type: string
          maxLength: 10
        surface:
          type: string
          maxLength: 20
        planned_fee:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        date_planned:
          type: string
          format: date
          nullable: true
        status:
          type: string
          description: planned, completed
          maxLength: 30
        est_primary_ins:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        est_secondary_ins:
          type: string
          format: decimal
          pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
        plan_name:
          type: string
          maxLength: 255
        priority:
          type: integer
          maximum: 2147483647
          minimum: 0
        custom_data: {}
    TreatmentPlanStatusEnum:
      enum:
        - active
        - accepted
        - declined
        - completed
        - archived
      type: string
      description: |-
        * `active` - Active
        * `accepted` - Accepted
        * `declined` - Declined
        * `completed` - Completed
        * `archived` - Archived
    TreatmentPlanWrite:
      type: object
      description: >-
        Write serializer for creating and updating treatment plans.


        The ``patient`` field is excluded because it is set from the URL path.

        Supports nested ``items`` for creating a plan with line items in one
        request.


        GH-711: ``patient`` / ``provider`` FKs are practice-validated.
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        name:
          type: string
          maxLength: 200
        note:
          type: string
        status:
          $ref: '#/components/schemas/TreatmentPlanStatusEnum'
        date_accepted:
          type: string
          format: date
          nullable: true
        custom_data: {}
        items:
          type: array
          items:
            $ref: '#/components/schemas/TreatmentPlanItemWrite'
      required:
        - name
    TreatmentPlanWriteRequest:
      type: object
      description: >-
        Write serializer for creating and updating treatment plans.


        The ``patient`` field is excluded because it is set from the URL path.

        Supports nested ``items`` for creating a plan with line items in one
        request.


        GH-711: ``patient`` / ``provider`` FKs are practice-validated.
      properties:
        provider:
          type: string
          format: uuid
          nullable: true
        name:
          type: string
          minLength: 1
          maxLength: 200
        note:
          type: string
        status:
          $ref: '#/components/schemas/TreatmentPlanStatusEnum'
        date_accepted:
          type: string
          format: date
          nullable: true
        custom_data: {}
        items:
          type: array
          items:
            $ref: '#/components/schemas/TreatmentPlanItemWriteRequest'
      required:
        - name
    User:
      type: object
      description: |-
        Public representation of a user (excludes sensitive fields).

        ``is_staff`` and ``is_superuser`` are exposed read-only so client apps
        (notably the internal support tool) can gate staff-only / superuser-only
        UI on the authenticated user's own ``/me/`` response, and so the support
        tool's Staff tab can distinguish the two populations in its list view.
        Both fields are in ``read_only_fields`` so a regular PATCH cannot
        escalate privileges.

        ``current_practice_id`` (read-only) is populated when the request is
        authenticated via a ServiceToken — the long-lived OAuth refresh token
        issued to the importer / sync tools is bound to a single practice at
        consent time, and surfacing that binding lets clients drop their own
        multi-practice fallback logic. ``null`` for password-grant or any
        non-service-token authenticated request.

        ``practice_timezone`` (read-only) is the IANA timezone string of the
        practice this session is anchored to. The patient portal uses it to
        compute "today" / "tomorrow" against the office's calendar instead of
        the browser's — a remote staff member or a patient travelling out of
        state must still see the practice's perspective (per saved memory
        ``feedback_practice_tz_not_browser``). Resolution order:

        1. Service-token-bound practice (matches ``current_practice_id``).
        2. The user's first active ``PracticeMembership`` (covers the common
           patient-portal case where a portal account has exactly one
           membership, and the staff-token case where ``current_practice_id``
           is null but the user still has a single anchor).
        3. ``null`` when the user has no active membership at all.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        email:
          type: string
          format: email
          readOnly: true
          title: Email address
        first_name:
          type: string
          maxLength: 150
        last_name:
          type: string
          maxLength: 150
        is_patient:
          type: boolean
          readOnly: true
          description: Designates whether this user is a patient with portal access.
        is_staff:
          type: boolean
          readOnly: true
        is_superuser:
          type: boolean
          readOnly: true
          title: Superuser status
          description: >-
            Designates that this user has all permissions without explicitly
            assigning them.
        is_active:
          type: boolean
          readOnly: true
        date_joined:
          type: string
          format: date-time
          readOnly: true
        last_login:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        last_seen_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: >-
            Last time this user was seen online (updated on WebSocket
            disconnect).
        first_run_tour_completed_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: >-
            Timestamp at which the user dismissed the in-app first-run guided
            tour.  Null for users who have never completed it.
        preference_data:
          type: string
          readOnly: true
        memberships:
          type: array
          items:
            $ref: '#/components/schemas/PracticeMembership'
          readOnly: true
        client_memberships:
          type: array
          items:
            $ref: '#/components/schemas/ClientMembership'
          readOnly: true
        max_session_hours:
          type: string
          readOnly: true
        current_practice_id:
          type: string
          format: uuid
          nullable: true
          readOnly: true
          description: >-
            UUID of the practice this request's service token is bound to, or
            null when the request is not authenticated via a service token.
        practice_timezone:
          type: string
          nullable: true
          readOnly: true
          description: >-
            IANA timezone string (e.g. ``America/Los_Angeles``) of the practice
            this session is anchored to. Falls back to the user's first active
            membership when no service-token binding is present, and is null
            when the user has no active membership.
      required:
        - client_memberships
        - current_practice_id
        - date_joined
        - email
        - first_name
        - first_run_tour_completed_at
        - id
        - is_active
        - is_patient
        - is_staff
        - is_superuser
        - last_login
        - last_name
        - last_seen_at
        - max_session_hours
        - memberships
        - practice_timezone
        - preference_data
    UserRequest:
      type: object
      description: |-
        Public representation of a user (excludes sensitive fields).

        ``is_staff`` and ``is_superuser`` are exposed read-only so client apps
        (notably the internal support tool) can gate staff-only / superuser-only
        UI on the authenticated user's own ``/me/`` response, and so the support
        tool's Staff tab can distinguish the two populations in its list view.
        Both fields are in ``read_only_fields`` so a regular PATCH cannot
        escalate privileges.

        ``current_practice_id`` (read-only) is populated when the request is
        authenticated via a ServiceToken — the long-lived OAuth refresh token
        issued to the importer / sync tools is bound to a single practice at
        consent time, and surfacing that binding lets clients drop their own
        multi-practice fallback logic. ``null`` for password-grant or any
        non-service-token authenticated request.

        ``practice_timezone`` (read-only) is the IANA timezone string of the
        practice this session is anchored to. The patient portal uses it to
        compute "today" / "tomorrow" against the office's calendar instead of
        the browser's — a remote staff member or a patient travelling out of
        state must still see the practice's perspective (per saved memory
        ``feedback_practice_tz_not_browser``). Resolution order:

        1. Service-token-bound practice (matches ``current_practice_id``).
        2. The user's first active ``PracticeMembership`` (covers the common
           patient-portal case where a portal account has exactly one
           membership, and the staff-token case where ``current_practice_id``
           is null but the user still has a single anchor).
        3. ``null`` when the user has no active membership at all.
      properties:
        first_name:
          type: string
          minLength: 1
          maxLength: 150
        last_name:
          type: string
          minLength: 1
          maxLength: 150
      required:
        - first_name
        - last_name
    ValidateAppointmentIssue:
      type: object
      properties:
        level:
          type: string
        code:
          type: string
        message:
          type: string
      required:
        - code
        - level
        - message
    ValidateAppointmentRequestRequest:
      type: object
      properties:
        operatory:
          type: string
          format: uuid
          nullable: true
        date_time:
          type: string
          format: date-time
          description: ISO 8601 with timezone offset.
        duration_minutes:
          type: integer
          minimum: 1
        provider:
          type: string
          format: uuid
          nullable: true
        exclude_appointment:
          type: string
          format: uuid
          nullable: true
      required:
        - date_time
        - duration_minutes
    ValidateAppointmentResponse:
      type: object
      properties:
        valid:
          type: boolean
        issues:
          type: array
          items:
            $ref: '#/components/schemas/ValidateAppointmentIssue'
      required:
        - issues
        - valid
    VitalSign:
      type: object
      description: Read serializer for vital signs with denormalized provider name.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        patient:
          type: string
          format: uuid
          readOnly: true
        appointment:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        provider:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        provider_name:
          type: string
          nullable: true
          readOnly: true
        reading_date:
          type: string
          format: date-time
          readOnly: true
          description: When the vitals were taken
        bp_systolic:
          type: integer
          readOnly: true
          nullable: true
          description: Systolic blood pressure (mmHg)
        bp_diastolic:
          type: integer
          readOnly: true
          nullable: true
          description: Diastolic blood pressure (mmHg)
        pulse:
          type: integer
          readOnly: true
          nullable: true
          description: Pulse rate (bpm)
        respiration:
          type: integer
          readOnly: true
          nullable: true
          description: Respiration rate (breaths/min)
        o2_saturation:
          type: integer
          readOnly: true
          nullable: true
          description: Oxygen saturation (%)
        temperature:
          type: string
          format: decimal
          pattern: ^-?\d{0,3}(?:\.\d{0,1})?$
          readOnly: true
          nullable: true
          description: Temperature (F)
        note:
          type: string
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - appointment
        - bp_diastolic
        - bp_systolic
        - created
        - id
        - modified
        - note
        - o2_saturation
        - patient
        - provider
        - provider_name
        - pulse
        - reading_date
        - respiration
        - temperature
    VitalSignWrite:
      type: object
      description: |-
        Write serializer for creating and updating vital sign readings.

        The ``patient`` field is excluded because it is set from the URL path
        by the view's ``perform_create`` method.

        GH-711: ``patient`` / ``appointment`` / ``provider`` FKs are
        practice-validated.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        appointment:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        reading_date:
          type: string
          format: date-time
          description: When the vitals were taken
        bp_systolic:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Systolic blood pressure (mmHg)
        bp_diastolic:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Diastolic blood pressure (mmHg)
        pulse:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Pulse rate (bpm)
        respiration:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Respiration rate (breaths/min)
        o2_saturation:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Oxygen saturation (%)
        temperature:
          type: string
          format: decimal
          pattern: ^-?\d{0,3}(?:\.\d{0,1})?$
          nullable: true
          description: Temperature (F)
        note:
          type: string
        custom_data: {}
      required:
        - id
        - reading_date
    VitalSignWriteRequest:
      type: object
      description: |-
        Write serializer for creating and updating vital sign readings.

        The ``patient`` field is excluded because it is set from the URL path
        by the view's ``perform_create`` method.

        GH-711: ``patient`` / ``appointment`` / ``provider`` FKs are
        practice-validated.
      properties:
        appointment:
          type: string
          format: uuid
          nullable: true
        provider:
          type: string
          format: uuid
          nullable: true
        reading_date:
          type: string
          format: date-time
          description: When the vitals were taken
        bp_systolic:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Systolic blood pressure (mmHg)
        bp_diastolic:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Diastolic blood pressure (mmHg)
        pulse:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Pulse rate (bpm)
        respiration:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Respiration rate (breaths/min)
        o2_saturation:
          type: integer
          maximum: 32767
          minimum: 0
          nullable: true
          description: Oxygen saturation (%)
        temperature:
          type: string
          format: decimal
          pattern: ^-?\d{0,3}(?:\.\d{0,1})?$
          nullable: true
          description: Temperature (F)
        note:
          type: string
        custom_data: {}
      required:
        - reading_date
    VoicemailInboxRow:
      type: object
      description: |-
        Serializer for a single voicemail-inbox row.

        Output-only.  ``audio_url`` is a short-lived presigned URL — the
        same pattern :class:`DocumentSerializer` uses for ``download_url``.
        ``transcript`` / ``transcript_confidence`` come from the latest
        :class:`apps.doccenter.models.DocumentOcrResult` for the document
        (the api#347 transcribe task writes to that table).
      properties:
        document_id:
          type: string
          format: uuid
          readOnly: true
        caller_id:
          type: string
          readOnly: true
        received_at:
          type: string
          format: date-time
          readOnly: true
        audio_url:
          type: string
          readOnly: true
          nullable: true
        transcript:
          type: string
          readOnly: true
        transcript_confidence:
          type: number
          format: double
          readOnly: true
          nullable: true
        current_patient_id:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        suggested_patients:
          type: array
          items:
            $ref: '#/components/schemas/VoicemailSuggestedPatient'
          readOnly: true
        resolved_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        processing_state:
          type: string
          readOnly: true
          nullable: true
        processing_error:
          type: string
          readOnly: true
      required:
        - audio_url
        - caller_id
        - current_patient_id
        - document_id
        - processing_error
        - processing_state
        - received_at
        - resolved_at
        - suggested_patients
        - transcript
        - transcript_confidence
    VoicemailSuggestedPatient:
      type: object
      description: |-
        One ranked patient suggestion for an un-linked voicemail.

        Output-only — never bound to model state.  The view builds the
        payload from :func:`apps.patients.utils.find_patients_by_phone` so
        the contract sits with the serializer rather than the queryset.
      properties:
        patient_id:
          type: string
          format: uuid
          readOnly: true
        display_name:
          type: string
          readOnly: true
        match_reason:
          type: string
          readOnly: true
          description: >-
            Which patient phone field matched the caller-ID: ``phone_mobile`` |
            ``phone_home`` | ``phone_work``.
      required:
        - display_name
        - match_reason
        - patient_id
    VoteRequest:
      type: object
      description: Accepts an option_id for casting a vote.
      properties:
        option_id:
          type: string
          format: uuid
      required:
        - option_id
    WebhookAck:
      type: object
      description: Minimal ack returned to Stripe after a successful webhook delivery.
      properties:
        status:
          type: string
      required:
        - status
    WebhookDelivery:
      type: object
      description: Read-only serializer for webhook delivery logs.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        event_type:
          type: string
          readOnly: true
        payload:
          readOnly: true
        response_status:
          type: integer
          readOnly: true
          nullable: true
        response_body:
          type: string
          readOnly: true
          description: Truncated to 1KB
        attempts:
          type: integer
          readOnly: true
        next_retry_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        status:
          allOf:
            - $ref: '#/components/schemas/WebhookDeliveryStatusEnum'
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
      required:
        - attempts
        - created
        - event_type
        - id
        - next_retry_at
        - payload
        - response_body
        - response_status
        - status
    WebhookDeliveryStatusEnum:
      enum:
        - pending
        - delivered
        - failed
        - exhausted
      type: string
      description: |-
        * `pending` - Pending
        * `delivered` - Delivered
        * `failed` - Failed
        * `exhausted` - Exhausted
    WebhookSubscription:
      type: object
      description: >-
        Read serializer for webhook subscriptions — includes related names and
        status fields.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        application:
          type: integer
        application_name:
          type: string
          readOnly: true
        client:
          type: string
          format: uuid
        client_name:
          type: string
          readOnly: true
        target_url:
          type: string
          format: uri
          maxLength: 2048
        events:
          description: List of event patterns, e.g. ['patient.created', 'appointment.*']
        all_practices:
          type: boolean
          description: Subscribe to events across all practices, or select specific ones
        practices:
          type: array
          items:
            type: string
            format: uuid
          description: Specific practices (when all_practices=False)
        is_active:
          type: boolean
        is_verified:
          type: boolean
          readOnly: true
          description: URL ownership verified via challenge/response
        consecutive_failures:
          type: integer
          readOnly: true
        last_failure_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        disabled_reason:
          type: string
          readOnly: true
        created:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        modified:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        custom_data: {}
      required:
        - application
        - application_name
        - client
        - client_name
        - consecutive_failures
        - created
        - disabled_reason
        - id
        - is_verified
        - last_failure_at
        - modified
        - target_url
    WebhookSubscriptionCreate:
      type: object
      description: >-
        Write serializer for creating/updating webhook subscriptions — validates
        URL and event patterns.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        application:
          type: integer
        client:
          type: string
          format: uuid
        target_url:
          type: string
          format: uri
          maxLength: 2048
        events:
          description: List of event patterns, e.g. ['patient.created', 'appointment.*']
        all_practices:
          type: boolean
          description: Subscribe to events across all practices, or select specific ones
        practices:
          type: array
          items:
            type: string
            format: uuid
          description: Specific practices (when all_practices=False)
        custom_data: {}
      required:
        - application
        - client
        - id
        - target_url
    WebhookSubscriptionCreateRequest:
      type: object
      description: >-
        Write serializer for creating/updating webhook subscriptions — validates
        URL and event patterns.
      properties:
        application:
          type: integer
        client:
          type: string
          format: uuid
        target_url:
          type: string
          format: uri
          minLength: 1
          maxLength: 2048
        events:
          description: List of event patterns, e.g. ['patient.created', 'appointment.*']
        all_practices:
          type: boolean
          description: Subscribe to events across all practices, or select specific ones
        practices:
          type: array
          items:
            type: string
            format: uuid
          description: Specific practices (when all_practices=False)
        custom_data: {}
      required:
        - application
        - client
        - target_url
    XrayImageUpload:
      type: object
      description: Upload serializer for images from the desktop app.
      properties:
        file:
          type: string
          format: uri
        template_slot:
          type: string
          format: uuid
          nullable: true
          description: Which slot in the exam template grid this image fills
        image_type:
          oneOf:
            - $ref: '#/components/schemas/ImageTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        capture_device:
          type: string
          description: Device name from browser getUserMedia
          maxLength: 255
        tooth_numbers:
          description: 'Universal numbering: 1-32 permanent, A-T primary'
        description:
          type: string
          description: Short description/label for the image
          maxLength: 255
        notes:
          type: string
      required:
        - file
    XrayImageUploadRequest:
      type: object
      description: Upload serializer for images from the desktop app.
      properties:
        file:
          type: string
          format: binary
        template_slot:
          type: string
          format: uuid
          nullable: true
          description: Which slot in the exam template grid this image fills
        image_type:
          oneOf:
            - $ref: '#/components/schemas/ImageTypeEnum'
            - $ref: '#/components/schemas/BlankEnum'
        capture_device:
          type: string
          description: Device name from browser getUserMedia
          maxLength: 255
        tooth_numbers:
          description: 'Universal numbering: 1-32 permanent, A-T primary'
        description:
          type: string
          description: Short description/label for the image
          maxLength: 255
        notes:
          type: string
      required:
        - file
    _AdjustmentType:
      type: object
      description: One adjustment-type sub-row inside a per-practice rollup.
      properties:
        adj_type:
          type: string
        amount:
          type: string
        count:
          type: integer
      required:
        - adj_type
        - amount
        - count
    _AgingBuckets:
      type: object
      description: Four-bucket aging totals shared by summary + per-practice rows.
      properties:
        total_ar:
          type: string
        current:
          type: string
        days_31_60:
          type: string
        days_61_90:
          type: string
        over_90:
          type: string
        patient_count:
          type: integer
      required:
        - current
        - days_31_60
        - days_61_90
        - over_90
        - patient_count
        - total_ar
    _CarrierMixRow:
      type: object
      description: One carrier inside a practice's payer-mix list.
      properties:
        carrier_id:
          type: string
          nullable: true
        carrier_name:
          type: string
        patient_count:
          type: integer
        share_pct:
          type: string
      required:
        - carrier_id
        - carrier_name
        - patient_count
        - share_pct
    _CaseAcceptanceTotals:
      type: object
      description: |-
        Presented vs. accepted treatment-plan totals.

        api#506 adds two extras to the legacy contract:

        * ``same_visit_accepted_pct`` -- of the presented amount, the
          share that was accepted on the same date the plan was
          presented (parent ``TreatmentPlan.date_accepted`` matches
          ``date_created``).
        * ``delayed_accepted_pct`` -- the remaining accepted slice that
          landed on a later date.  Together with the not-accepted slice
          (``100 - acceptance_rate_pct``) the three numbers compose a
          stacked-bar acceptance breakdown.
      properties:
        presented_amount:
          type: string
        accepted_amount:
          type: string
        acceptance_rate_pct:
          type: string
        same_visit_accepted_pct:
          type: string
        delayed_accepted_pct:
          type: string
      required:
        - acceptance_rate_pct
        - accepted_amount
        - delayed_accepted_pct
        - presented_amount
        - same_visit_accepted_pct
    _InboxOwnerUser:
      type: object
      description: |-
        Compact user envelope rendered in the GET response.

        The UI needs ``email`` and a display name to render the configured
        owners as chips next to their IDs without firing a follow-up
        ``GET /staff/<id>/``.  ``full_name`` is computed from
        :meth:`User.get_full_name` so we mirror the rest of the codebase's
        name-rendering rule (falls back to the empty string for accounts
        with no first/last name set).
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        email:
          type: string
          format: email
          readOnly: true
        full_name:
          type: string
          readOnly: true
      required:
        - email
        - full_name
        - id
    _InsurancePlanVerificationSummary:
      type: object
      description: >-
        Tiny read-only summary of an :class:`InsurancePlan` for verification
        responses.


        Front-desk lists want a human label without a second round-trip; this

        exposes just the fields needed to render a row.  Not a full plan

        serializer -- consumers who want the whole plan should hit the plan

        detail endpoint.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        plan_name:
          type: string
          readOnly: true
        group_number:
          type: string
          readOnly: true
        carrier_name:
          type: string
          readOnly: true
      required:
        - carrier_name
        - group_number
        - id
        - plan_name
    _MessagingSettings:
      type: object
      description: |-
        Read/write payload for the per-practice messaging settings.

        On read we return both the raw ID list (the canonical setting) and
        a derived ``inbox_owner_users`` array of compact envelopes so the
        UI can render the chips without a second round-trip.  On write the
        caller PATCHes only ``inbox_owner_user_ids`` -- the derived
        envelopes are read-only.
      properties:
        inbox_owner_user_ids:
          type: array
          items:
            type: string
            format: uuid
          description: >-
            Users to route new patient-portal threads to.  Empty list restores
            the historical ADMIN / OFFICE_MANAGER fan-out.
        inbox_owner_users:
          type: array
          items:
            $ref: '#/components/schemas/_InboxOwnerUser'
          readOnly: true
      required:
        - inbox_owner_user_ids
        - inbox_owner_users
    _PatientSendMessageRequest:
      type: object
      description: |-
        Body for ``POST /portal/messaging/conversation/<id>/messages/``.

        A single non-blank ``body`` string.  ``.strip()``ed before
        persisting so a payload of all whitespace fails the ``min_length``
        gate rather than silently producing an empty message row.
      properties:
        body:
          type: string
          minLength: 1
          description: Message body.  Must be non-empty after whitespace trim.
          maxLength: 5000
      required:
        - body
    _PatientStartConversationRequest:
      type: object
      description: |-
        Body for ``POST /portal/messaging/conversation/``.

        ``body`` is required (the first message that opens the thread);
        ``subject`` is optional and falls back to a preview of the body.
        The patient cannot pick participants, conversation_type, or
        operatory -- those are set server-side based on the patient's
        practice.
      properties:
        body:
          type: string
          minLength: 1
          description: First message in the new conversation.  Non-empty after trim.
          maxLength: 5000
        subject:
          type: string
          default: ''
          description: >-
            Optional thread title.  Defaults to the first 80 characters of
            ``body`` when omitted or blank -- so the staff inbox always has a
            usable label.
          maxLength: 255
      required:
        - body
    _PatientVerificationSummary:
      type: object
      description: |-
        Tiny read-only summary of a :class:`Patient`.

        Emits the patient's initials rather than the full name so list
        responses don't leak PHI in, e.g., server logs or paginated audit
        previews.  Callers that need the full name must fetch the patient
        separately.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        initials:
          type: string
          readOnly: true
      required:
        - id
        - initials
    _ProductionTotals:
      type: object
      description: Production / collections / adjustment totals shared by summary + rows.
      properties:
        production:
          type: string
        collections:
          type: string
        adjustments:
          type: string
        net_production:
          type: string
        collection_pct:
          type: string
      required:
        - adjustments
        - collection_pct
        - collections
        - net_production
        - production
    _ProviderProductionRow:
      type: object
      description: One provider inside a practice's production breakdown.
      properties:
        provider_id:
          type: string
        provider_name:
          type: string
        production:
          type: string
        collections:
          type: string
        adjustments:
          type: string
        procedure_count:
          type: integer
        hours:
          type: string
        production_per_hour:
          type: string
      required:
        - adjustments
        - collections
        - hours
        - procedure_count
        - production
        - production_per_hour
        - provider_id
        - provider_name
    _RecareTotals:
      type: object
      description: |-
        Recare compliance bucket counts.

        api#507 adds:

        * ``without_active_recare_count`` -- patients with no active recall
          row, populated only when ``?include_without_active_recare=true``
          is passed.  Always present in the response (zero by default) so
          consumers can render the field unconditionally.
        * ``avg_days_late_among_non_compliant`` -- mean ``(as_of - date_due)``
          across the practice's overdue rows, stringified to 2dp like the
          other money/percentage fields.
      properties:
        compliant_count:
          type: integer
        upcoming_count:
          type: integer
        overdue_count:
          type: integer
        without_active_recare_count:
          type: integer
        recall_total:
          type: integer
        compliance_rate_pct:
          type: string
        avg_days_late_among_non_compliant:
          type: string
      required:
        - avg_days_late_among_non_compliant
        - compliance_rate_pct
        - compliant_count
        - overdue_count
        - recall_total
        - upcoming_count
        - without_active_recare_count
    _VerifiedBySummary:
      type: object
      description: Tiny read-only summary of the user who captured the verification.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        display_name:
          type: string
          readOnly: true
      required:
        - display_name
        - id
    Inactive403Response:
      type: object
      description: >-
        Response body for an inactive-tenant 403.  Returned by every
        practice-scoped mutating endpoint when the practice or its parent client
        has ``is_active=False``.
      properties:
        detail:
          type: string
          description: Human-readable explanation. Stable English; not localised.
          example: This practice or its parent organization is disabled.
        code:
          type: string
          enum:
            - inactive_practice
            - inactive_client
          description: >-
            Machine-readable reason. ``inactive_practice`` means the practice
            itself is disabled; ``inactive_client`` means the parent client
            (organisation) is disabled and the cascade blocks every descendant
            write.
      required:
        - detail
        - code
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Paste an OAuth2 access token or JWT here.
    jwtAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    oauth2:
      type: oauth2
      description: >-
        OAuth2 authorization-code flow. The scope list below includes both the
        legacy ``<domain>:<action>`` scopes and the new ``<action>:<domain>``
        taxonomy introduced in api#124.  Per-endpoint requirements are published
        via the ``security`` block on each operation.
      flows:
        authorizationCode:
          authorizationUrl: /api/v1/oauth/authorize/
          tokenUrl: /api/v1/oauth/token/
          scopes:
            read: Read access
            write: Write access
            profile:read: View your name and email
            practice:read: View practice name, address, and hours
            practice:write: Update practice settings
            provider:read: View provider names and schedules
            provider:write: Manage providers and operatories
            scheduling:read: View appointments and schedule templates
            scheduling:write: Create and modify appointments
            patient:read: View patient demographics
            patient:write: Create and update patient records
            clinical:read: View clinical notes, conditions, and treatment plans
            clinical:write: Create and update clinical records
            prescription:read: View prescriptions
            prescription:write: Create and update prescriptions
            imaging:read: View dental images and exams
            imaging:write: Upload and manage dental images
            billing:read: View balances, payments, and adjustments
            billing:write: Record payments and adjustments
            insurance:read: View insurance plans, claims, and benefits
            insurance:write: Create and update insurance information
            doccenter:read: View uploaded documents
            doccenter:write: Upload and manage documents
            import:write: Upload practice data during migration
            patient-intake:write: Create and update patients from intake kiosk
            preferences: Read and write your app preferences
            webhooks:manage: Manage webhook subscriptions
            service_token: Request a long-lived service token for unattended access
            act_on_behalf: Act on your behalf when you sign in to this app
            mcp:read: Call read-only tools on the Model Context Protocol server
            fhir:read: Read clinical data via the FHIR R4 API
            read:patients: View patient demographics and contact information
            read:appointments: View appointments, schedule templates, and availability
            read:billing: View balances, payments, and adjustments
            read:insurance: View insurance carriers, plans, benefits, and claims
            read:clinical: >-
              View clinical notes, procedures, perio, conditions, allergies, and
              medications
            read:messaging: View messages and communication history
            read:documents: View uploaded documents and dental images
            read:staff: View practice members and roles
            read:reports: View aggregated reports and analytics
            write:patients: Create and update patient records
            write:appointments: Create, modify, and cancel appointments
            write:billing: Record payments, adjustments, and checkout
            write:insurance: Create and update insurance plans, claims, and benefits
            write:clinical: >-
              Create and update clinical notes, procedures, perio, conditions,
              allergies, and medications
            write:messaging: Send messages and manage conversations
            write:documents: Upload and manage documents and dental images
            write:staff: Invite, update, and remove practice members
            admin:practices: Administer practices (staff only)
            admin:users: Administer users (staff only)
            admin:billing: Administer platform billing (staff only)
            admin:audit: View the audit log (staff only)
            sync:patients: Bulk import patient records (first-party tools only)
            sync:appointments: Bulk import appointments (first-party tools only)
            sync:billing: Bulk import billing data (first-party tools only)
            sync:insurance: Bulk import insurance data (first-party tools only)
            sync:clinical: Bulk import clinical data (first-party tools only)
            sync:documents: Bulk import documents (first-party tools only)
            sync:staff: Bulk import staff records (first-party tools only)
            mcp:write: Call write tools on the Model Context Protocol server
            reports:read: View every category of reports and analytics
            reports:production:read: View production and collections reports
            reports:ar:read: View accounts-receivable, adjustments, and balance reports
            reports:patients:read: View patient history, recall, and reactivation reports
            reports:operations:read: View day-sheet, scheduling, and provider-utilization reports
            reports:compliance:read: View HIPAA audit-log and compliance reports
servers:
  - url: http://localhost:8000
    description: Current server
tags:
  - name: Scheduling
    description: Appointments, day sheet, templates, overrides, recalls
  - name: Patients
    description: Patient demographics and search
  - name: Clinical
    description: >-
      Notes, procedures, treatment plans, perio, vitals, conditions,
      medications, allergies
  - name: Insurance
    description: Carriers, plans, benefits, fee schedules, claims, estimation
  - name: Billing
    description: Balances, payments, adjustments, checkout, production reports
  - name: Imaging
    description: Sources, exam templates, exams, images, x-ray capture
  - name: Documents
    description: Document center, inbox, scan-to-email
  - name: Providers
    description: Providers and operatories
  - name: Staff
    description: Practice members, invitations, roles
  - name: Account
    description: User profile, passkeys, preferences
  - name: Practices
    description: Practice management and logo upload
  - name: Import
    description: Data migration sessions and batches
  - name: Webhooks
    description: Webhook subscriptions and delivery
  - name: OAuth2
    description: Authorization and token management
