openapi: 3.0.3
info:
  title: Books API
  version: "1.0.0"
  description: |
    An API for a shared *Book* — a financial ledger for one person or small
    business, backed by a Clerk Organization so it can have collaborators.
    A Book owns a set of ledger-api accounts and exposes a simplified,
    type-aware transaction model (`expense`/`income`/`transfer`) so callers
    never have to think in raw double-entry debits and credits.

    Every route acts on the caller's *active* Clerk Organization: the Book
    is always the verified JWT's `org_id` claim, never a URL or body
    parameter. There is no Book-ID parameter anywhere to get an
    authorization check wrong on.

    Authenticate with a Clerk session token, issued against the `books-api`
    JWT template (org claims enabled), sent as `Authorization: Bearer
    <token>`.

    See `books-api-plan.md` and `docs/projects/books-api/design.md` in the
    `operations` repo for the full domain design this implements.
  contact:
    name: Pete Shepley
    url: https://peteshepley.com
  license:
    name: Source
    url: https://github.com/PeteShepley/books-api

servers:
  - url: https://books.api.peteshepley.com
    description: Production
  # Default API Gateway invoke URL is only known after the first `tofu
  # apply` of infra/apis/books-api — add it here once deployed, same as
  # ledger-api's second `servers` entry.

security:
  - clerkAuth: []

tags:
  - name: Book
    description: The active Book's own metadata.
  - name: Accounts
    description: Book-scoped accounts and categories (backed by namespaced ledger-api accounts).
  - name: Transactions
    description: Typed transactions (expense/income/transfer) posted into the Book's ledger.

paths:
  /book:
    get:
      operationId: getBook
      tags: [Book]
      summary: Get the active Book
      responses:
        "200":
          description: The active Book.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Book"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      operationId: createBook
      tags: [Book]
      summary: Create the active Book (one-time initializer)
      description: >-
        Provisions books-api-plan.md's 15-account default chart of accounts
        against ledger-api. Not an upsert — fails with 422 if a Book
        already exists for this organization.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BookInput"
      responses:
        "201":
          description: Book created, with its provisioned accounts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  book:
                    $ref: "#/components/schemas/Book"
                  accounts:
                    type: array
                    items:
                      $ref: "#/components/schemas/Account"
        "422":
          $ref: "#/components/responses/Unprocessable"

  /book/accounts:
    get:
      operationId: listAccounts
      tags: [Accounts]
      summary: List the Book's accounts and categories
      responses:
        "200":
          description: All accounts/categories.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Account"
    post:
      operationId: createAccount
      tags: [Accounts]
      summary: Add a custom account or category
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AccountInput"
      responses:
        "201":
          description: Account/category created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Account"
        "422":
          $ref: "#/components/responses/Unprocessable"

  /book/accounts/{id}:
    patch:
      operationId: patchAccount
      tags: [Accounts]
      summary: Rename or archive an account/category
      description: >-
        books-api metadata only — never touches ledger-api. ledger-api's own
        account name is treated as immutable once created.
      parameters:
        - $ref: "#/components/parameters/AccountId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AccountPatch"
      responses:
        "200":
          description: Updated account/category.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Account"
        "404":
          $ref: "#/components/responses/NotFound"

  /book/transactions:
    get:
      operationId: listTransactions
      tags: [Transactions]
      summary: List the Book's transactions, most recent first
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            default: 50
      responses:
        "200":
          description: Transaction summaries.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/TransactionSummary"
    post:
      operationId: createTransaction
      tags: [Transactions]
      summary: Record a transaction
      description: >-
        `type` picks how the signed ledger-api entries are derived — see
        books-api-plan.md 1.4. `id` is required and doubles as ledger-api's
        idempotency key: a retried POST with the same `id` returns the
        original, `200`, never a duplicate post.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TransactionInput"
      responses:
        "201":
          description: Transaction posted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transaction"
        "200":
          description: Idempotent replay of an already-posted transaction.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transaction"
        "422":
          $ref: "#/components/responses/Unprocessable"

  /book/transactions/{id}:
    get:
      operationId: getTransaction
      tags: [Transactions]
      summary: Get a transaction with its entries
      parameters:
        - $ref: "#/components/parameters/TransactionId"
      responses:
        "200":
          description: Transaction detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transaction"
        "404":
          $ref: "#/components/responses/NotFound"

  /book/transactions/{id}/void:
    post:
      operationId: voidTransaction
      tags: [Transactions]
      summary: Void a transaction
      description: >-
        Posts a new, equal-and-opposite transaction and links the two
        records — books-api-plan.md 1.5's "void, not edit" rule. No
        in-place edit exists; correcting a mistake means voiding the
        original and recording a new one.
      parameters:
        - $ref: "#/components/parameters/TransactionId"
      responses:
        "201":
          description: The new reversing transaction.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transaction"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          description: Already voided.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

components:
  securitySchemes:
    clerkAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        A Clerk session token issued against the `books-api` JWT template
        (org claims enabled). Verified by API Gateway's JWT authorizer
        before the request ever reaches the Lambda; the active organization
        (`org_id` claim) is the Book every route acts on.

  parameters:
    AccountId:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: An account/category's slug (e.g. `checking`, `groceries`), not ledger-api's internal id.
    TransactionId:
      name: id
      in: path
      required: true
      schema:
        type: string

  responses:
    NotFound:
      description: Not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unprocessable:
      description: >-
        Validation failure — split amounts don't sum to the total, unknown
        or archived account/category, bad currency code, or a duplicate
        Book/account id.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string

    BookInput:
      type: object
      required: [currency]
      properties:
        currency:
          type: string
          description: ISO 4217, e.g. `USD`. One currency per Book.
          example: USD
    Book:
      allOf:
        - $ref: "#/components/schemas/BookInput"
        - type: object
          properties:
            org_id:
              type: string
            chart_version:
              type: integer
            created_at:
              type: string
              format: date-time

    AccountType:
      type: string
      enum: [asset, liability, income, expense]

    AccountInput:
      type: object
      required: [id, display_name, type]
      properties:
        id:
          type: string
          description: Slug — lowercase letters, numbers, and hyphens only.
          example: side-hustle
        display_name:
          type: string
        type:
          $ref: "#/components/schemas/AccountType"
    Account:
      allOf:
        - $ref: "#/components/schemas/AccountInput"
        - type: object
          properties:
            archived:
              type: boolean
            balance:
              type: integer
              description: Current balance, minor units, fetched live from ledger-api.
            created_at:
              type: string
              format: date-time

    AccountPatch:
      type: object
      properties:
        display_name:
          type: string
        archived:
          type: boolean

    TransactionType:
      type: string
      enum: [expense, income, transfer]

    Split:
      type: object
      required: [category_id, amount]
      properties:
        category_id:
          type: string
        amount:
          type: integer
          description: Positive, minor units (cents).

    TransactionInput:
      type: object
      required: [id, type]
      properties:
        id:
          type: string
          description: Client-supplied — also used as ledger-api's idempotency key.
        type:
          $ref: "#/components/schemas/TransactionType"
        date:
          type: string
          format: date
          description: Defaults to today (UTC) if omitted.
        description:
          type: string
        source_account_id:
          type: string
          description: expense, transfer
        destination_account_id:
          type: string
          description: income, transfer
        category_id:
          type: string
          description: income, or expense without splits
        amount:
          type: integer
          description: Positive, minor units. Required unless splits is used (expense).
        splits:
          type: array
          items:
            $ref: "#/components/schemas/Split"
          description: expense only — multiple categories from one source account, must sum to amount if both are given.

    Entry:
      type: object
      properties:
        account_id:
          type: string
          description: An account/category slug, not ledger-api's internal id.
        amount:
          type: integer
          description: Signed, minor units.
        effective_date:
          type: string
          format: date

    Transaction:
      type: object
      properties:
        id:
          type: string
        type:
          $ref: "#/components/schemas/TransactionType"
        description:
          type: string
        voids:
          type: string
          description: Set if this transaction is a void of another.
        voided_by:
          type: string
          description: Set if this transaction has itself been voided.
        effective_date:
          type: string
          format: date
        created_at:
          type: string
          format: date-time
        entries:
          type: array
          items:
            $ref: "#/components/schemas/Entry"

    TransactionSummary:
      type: object
      properties:
        id:
          type: string
        type:
          $ref: "#/components/schemas/TransactionType"
        description:
          type: string
        voids:
          type: string
        voided_by:
          type: string
        created_at:
          type: string
          format: date-time
