openapi: 3.0.3
info:
  title: Library Management Backend API
  description: |
    A library catalog and lending API: authors, books (with per-title copy
    counts), borrowers, and loans (checkout/return with due dates and
    derived active/overdue/returned status).

    ## Authentication
    Every `GET` endpoint is public (the catalog is meant to be browsable
    without an account). `POST`/`PUT`/`DELETE` endpoints require a JWT:
    call `POST /api/v1/auth/login` with the admin credentials, then send
    `Authorization: Bearer <token>` on every mutating request.

    ## Response envelope
    Every response — success or error — is wrapped the same way:
    `{"data": ..., "meta": {"requestId", "timestamp", "pagination"?}}` on
    success, `{"error": {"code", "message", "fields"?}, "meta": {...}}` on
    failure.
  version: 1.0.0
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

servers:
  - url: http://localhost:8080
    description: Local

tags:
  - name: Health
  - name: Auth
  - name: Authors
  - name: Books
  - name: Borrowers
  - name: Loans

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    Meta:
      type: object
      properties:
        requestId: { type: string }
        timestamp: { type: string, format: date-time }
        pagination:
          type: object
          nullable: true
          properties:
            page: { type: integer }
            pageSize: { type: integer }
            totalItems: { type: integer }
            totalPages: { type: integer }

    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string, example: validation_error }
            message: { type: string }
            fields:
              type: object
              additionalProperties: { type: string }
        meta: { $ref: '#/components/schemas/Meta' }

    Author:
      type: object
      properties:
        id: { type: integer, format: int64 }
        name: { type: string }
        bio: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    Book:
      type: object
      properties:
        id: { type: integer, format: int64 }
        isbn: { type: string }
        title: { type: string }
        authorId: { type: integer, format: int64 }
        authorName: { type: string, readOnly: true }
        genre: { type: string }
        publishedYear: { type: integer }
        totalCopies: { type: integer }
        availableCopies: { type: integer, readOnly: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    Borrower:
      type: object
      properties:
        id: { type: integer, format: int64 }
        name: { type: string }
        email: { type: string, format: email }
        phone: { type: string }
        membershipDate: { type: string, format: date-time }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    Loan:
      type: object
      properties:
        id: { type: integer, format: int64 }
        bookId: { type: integer, format: int64 }
        bookTitle: { type: string, readOnly: true }
        borrowerId: { type: integer, format: int64 }
        borrowerName: { type: string, readOnly: true }
        borrowedAt: { type: string, format: date-time }
        dueAt: { type: string, format: date-time }
        returnedAt: { type: string, format: date-time, nullable: true }
        status: { type: string, enum: [active, overdue, returned], readOnly: true }
        createdAt: { type: string, format: date-time }

  parameters:
    idParam:
      name: id
      in: path
      required: true
      schema: { type: integer, format: int64 }
    pageParam:
      name: page
      in: query
      schema: { type: integer, default: 1 }
    pageSizeParam:
      name: pageSize
      in: query
      schema: { type: integer, default: 20, maximum: 100 }

  responses:
    NotFound:
      description: Not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    ValidationError:
      description: Validation error
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Unauthorized:
      description: Missing or invalid bearer token
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Conflict:
      description: Conflict (e.g. no copies available, duplicate email/ISBN, already returned)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }

security: []

paths:
  /health:
    get:
      tags: [Health]
      summary: Shallow liveness check (always 200 while the process is up)
      responses:
        "200": { description: Healthy }

  /readiness:
    get:
      tags: [Health]
      summary: Readiness check — verifies the database is reachable
      responses:
        "200": { description: Ready }
        "503": { description: Database unreachable }

  /api/v1/auth/login:
    post:
      tags: [Auth]
      summary: Exchange admin credentials for a JWT
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, password]
              properties:
                username: { type: string, example: admin }
                password: { type: string, example: admin123 }
      responses:
        "200":
          description: Token issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      token: { type: string }
                      expiresAt: { type: string, format: date-time }
                      username: { type: string }
        "401": { $ref: '#/components/responses/Unauthorized' }

  /api/v1/authors:
    get:
      tags: [Authors]
      summary: List authors
      parameters:
        - $ref: '#/components/parameters/pageParam'
        - $ref: '#/components/parameters/pageSizeParam'
      responses:
        "200":
          description: Paginated author list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Author' } }
                  meta: { $ref: '#/components/schemas/Meta' }
    post:
      tags: [Authors]
      summary: Create an author
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                bio: { type: string }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties: { data: { $ref: '#/components/schemas/Author' } }
        "400": { $ref: '#/components/responses/ValidationError' }
        "401": { $ref: '#/components/responses/Unauthorized' }

  /api/v1/authors/{id}:
    parameters:
      - $ref: '#/components/parameters/idParam'
    get:
      tags: [Authors]
      summary: Get an author by ID
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties: { data: { $ref: '#/components/schemas/Author' } }
        "404": { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Authors]
      summary: Update an author
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                bio: { type: string }
      responses:
        "200": { description: Updated }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Authors]
      summary: Delete an author (fails if the author still has books — 409)
      security: [{ bearerAuth: [] }]
      responses:
        "204": { description: Deleted }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { $ref: '#/components/responses/NotFound' }
        "409": { $ref: '#/components/responses/Conflict' }

  /api/v1/books:
    get:
      tags: [Books]
      summary: List books
      parameters:
        - $ref: '#/components/parameters/pageParam'
        - $ref: '#/components/parameters/pageSizeParam'
        - { name: authorId, in: query, schema: { type: integer, format: int64 } }
        - { name: genre, in: query, schema: { type: string } }
        - { name: availableOnly, in: query, schema: { type: boolean } }
      responses:
        "200":
          description: Paginated book list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Book' } }
                  meta: { $ref: '#/components/schemas/Meta' }
    post:
      tags: [Books]
      summary: Add a book to the catalog
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, authorId]
              properties:
                isbn: { type: string }
                title: { type: string }
                authorId: { type: integer, format: int64 }
                genre: { type: string }
                publishedYear: { type: integer }
                totalCopies: { type: integer, default: 1 }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties: { data: { $ref: '#/components/schemas/Book' } }
        "400": { $ref: '#/components/responses/ValidationError' }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "409": { $ref: '#/components/responses/Conflict' }

  /api/v1/books/{id}:
    parameters:
      - $ref: '#/components/parameters/idParam'
    get:
      tags: [Books]
      summary: Get a book by ID
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties: { data: { $ref: '#/components/schemas/Book' } }
        "404": { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Books]
      summary: Update a book
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, authorId, totalCopies]
              properties:
                isbn: { type: string }
                title: { type: string }
                authorId: { type: integer, format: int64 }
                genre: { type: string }
                publishedYear: { type: integer }
                totalCopies: { type: integer }
      responses:
        "200": { description: Updated }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { $ref: '#/components/responses/NotFound' }
        "409": { $ref: '#/components/responses/Conflict' }
    delete:
      tags: [Books]
      summary: Delete a book (fails if it has loan history — 409)
      security: [{ bearerAuth: [] }]
      responses:
        "204": { description: Deleted }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { $ref: '#/components/responses/NotFound' }
        "409": { $ref: '#/components/responses/Conflict' }

  /api/v1/books/{id}/loans:
    parameters:
      - $ref: '#/components/parameters/idParam'
    get:
      tags: [Books]
      summary: Loan history for a book
      parameters:
        - $ref: '#/components/parameters/pageParam'
        - $ref: '#/components/parameters/pageSizeParam'
      responses:
        "200":
          description: Paginated loan list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Loan' } }
                  meta: { $ref: '#/components/schemas/Meta' }

  /api/v1/borrowers:
    get:
      tags: [Borrowers]
      summary: List borrowers
      parameters:
        - $ref: '#/components/parameters/pageParam'
        - $ref: '#/components/parameters/pageSizeParam'
      responses:
        "200":
          description: Paginated borrower list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Borrower' } }
                  meta: { $ref: '#/components/schemas/Meta' }
    post:
      tags: [Borrowers]
      summary: Register a borrower
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, email]
              properties:
                name: { type: string }
                email: { type: string, format: email }
                phone: { type: string }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties: { data: { $ref: '#/components/schemas/Borrower' } }
        "400": { $ref: '#/components/responses/ValidationError' }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "409": { $ref: '#/components/responses/Conflict' }

  /api/v1/borrowers/{id}:
    parameters:
      - $ref: '#/components/parameters/idParam'
    get:
      tags: [Borrowers]
      summary: Get a borrower by ID
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties: { data: { $ref: '#/components/schemas/Borrower' } }
        "404": { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Borrowers]
      summary: Update a borrower
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, email]
              properties:
                name: { type: string }
                email: { type: string, format: email }
                phone: { type: string }
      responses:
        "200": { description: Updated }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { $ref: '#/components/responses/NotFound' }
        "409": { $ref: '#/components/responses/Conflict' }
    delete:
      tags: [Borrowers]
      summary: Delete a borrower (fails if they have loan history — 409)
      security: [{ bearerAuth: [] }]
      responses:
        "204": { description: Deleted }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { $ref: '#/components/responses/NotFound' }
        "409": { $ref: '#/components/responses/Conflict' }

  /api/v1/borrowers/{id}/loans:
    parameters:
      - $ref: '#/components/parameters/idParam'
    get:
      tags: [Borrowers]
      summary: Loan history for a borrower
      parameters:
        - $ref: '#/components/parameters/pageParam'
        - $ref: '#/components/parameters/pageSizeParam'
      responses:
        "200":
          description: Paginated loan list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Loan' } }
                  meta: { $ref: '#/components/schemas/Meta' }

  /api/v1/loans:
    get:
      tags: [Loans]
      summary: List loans
      parameters:
        - $ref: '#/components/parameters/pageParam'
        - $ref: '#/components/parameters/pageSizeParam'
        - { name: bookId, in: query, schema: { type: integer, format: int64 } }
        - { name: borrowerId, in: query, schema: { type: integer, format: int64 } }
        - { name: status, in: query, schema: { type: string, enum: [active, overdue, returned] } }
      responses:
        "200":
          description: Paginated loan list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Loan' } }
                  meta: { $ref: '#/components/schemas/Meta' }
    post:
      tags: [Loans]
      summary: Borrow a book (checks out one available copy)
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [bookId, borrowerId]
              properties:
                bookId: { type: integer, format: int64 }
                borrowerId: { type: integer, format: int64 }
                loanPeriodDays: { type: integer, default: 14, description: "Defaults to 14 days if omitted or 0." }
      responses:
        "201":
          description: Loan created
          content:
            application/json:
              schema:
                type: object
                properties: { data: { $ref: '#/components/schemas/Loan' } }
        "400": { $ref: '#/components/responses/ValidationError' }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { $ref: '#/components/responses/NotFound' }
        "409":
          description: No copies of the book are currently available
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /api/v1/loans/{id}:
    parameters:
      - $ref: '#/components/parameters/idParam'
    get:
      tags: [Loans]
      summary: Get a loan by ID
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties: { data: { $ref: '#/components/schemas/Loan' } }
        "404": { $ref: '#/components/responses/NotFound' }

  /api/v1/loans/{id}/return:
    parameters:
      - $ref: '#/components/parameters/idParam'
    post:
      tags: [Loans]
      summary: Return a borrowed book (releases the copy back to the catalog)
      security: [{ bearerAuth: [] }]
      responses:
        "200":
          description: Returned
          content:
            application/json:
              schema:
                type: object
                properties: { data: { $ref: '#/components/schemas/Loan' } }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { $ref: '#/components/responses/NotFound' }
        "409":
          description: Loan has already been returned
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
