openapi: "3.1.0"
info:
  title: WorkRight Public API
  version: "1.0"
  description: |
    REST API for external integrators (Tally, Zoho, Razorpay X, etc.).
    Authenticated via Bearer tokens — create a token at
    Settings → API Tokens in the WorkRight dashboard.

    **Base URL (staging):** `https://api-staging.workright.in`
    **Base URL (production):** `https://api.workright.in`

    ## Authentication

    All requests must include:
    ```
    Authorization: Bearer wr_<32-hex-chars>
    ```

    Tokens are scoped to a single `(tenant, user)` pair. The token's
    capabilities determine what operations are allowed:

    | Capability | Allowed methods |
    |---|---|
    | `read`  | GET |
    | `write` | GET, POST, PATCH, DELETE |
    | `admin` | everything (reserved) |

    ## Rate limiting

    60 requests / minute per token. Exceeded requests receive `429 Too Many
    Requests` with a `Retry-After: 60` header.

    ## Webhook events

    Subscribe to domain events via `/api/v1/public/webhooks`. Supported
    event types:

    | Event | Trigger |
    |---|---|
    | `employee.created` | New employee added |
    | `employee.updated` | Employee profile changed |
    | `leave.requested` | Leave request submitted |
    | `leave.approved` | Leave request approved |
    | `leave.rejected` | Leave request rejected |
    | `expense.submitted` | Expense report submitted |
    | `expense.approved` | Expense report approved |
    | `expense.rejected` | Expense report rejected |
    | `invoice.created` | Invoice created |
    | `invoice.sent` | Invoice sent to customer |
    | `invoice.paid` | Invoice fully paid |
    | `bill.created` | Supplier bill created |
    | `bill.paid` | Supplier bill paid |
    | `payroll.run.locked` | Payroll run locked |
    | `period.locked` | Accounting period locked |

servers:
  - url: https://api-staging.workright.in
    description: Staging
  - url: https://api.workright.in
    description: Production

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: "wr_<32hex>"

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
          example: invalid_token
        message:
          type: string
          example: token not found or revoked

    Employee:
      type: object
      properties:
        id:
          type: integer
          format: int64
        email:
          type: string
          format: email
        first_name:
          type: string
        last_name:
          type: string
        employee_id:
          type: string
          nullable: true
        department:
          type: string
          nullable: true
        designation:
          type: string
          nullable: true
        role:
          type: string
          enum: [employee, manager, hr_manager, company_admin, icc_member, accountant]
        is_active:
          type: boolean
        hire_date:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    LeaveRequest:
      type: object
      properties:
        id:
          type: integer
          format: int64
        employee_id:
          type: integer
          format: int64
        leave_type_id:
          type: integer
          format: int64
        status:
          type: string
          enum: [pending, approved, rejected, cancelled]
        start_date:
          type: string
          format: date
        end_date:
          type: string
          format: date
        days:
          type: number
        reason:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time

    Expense:
      type: object
      properties:
        id:
          type: integer
          format: int64
        employee_id:
          type: integer
          format: int64
        category_id:
          type: integer
          format: int64
        status:
          type: string
          enum: [draft, submitted, approved, rejected, reimbursed]
        amount_paise:
          type: integer
          format: int64
          description: Amount in paise (1 INR = 100 paise)
        title:
          type: string
        description:
          type: string
          nullable: true
        expense_date:
          type: string
          format: date
        created_at:
          type: string
          format: date-time

    Invoice:
      type: object
      properties:
        id:
          type: integer
          format: int64
        invoice_number:
          type: string
        status:
          type: string
          enum: [draft, sent, partially_paid, paid, cancelled]
        contact_id:
          type: integer
          format: int64
        invoice_date:
          type: string
          format: date
        due_date:
          type: string
          format: date
          nullable: true
        total_paise:
          type: integer
          format: int64
        balance_paise:
          type: integer
          format: int64
        currency:
          type: string
          example: INR
        created_at:
          type: string
          format: date-time

    Bill:
      type: object
      properties:
        id:
          type: integer
          format: int64
        bill_number:
          type: string
        status:
          type: string
          enum: [draft, received, partially_paid, paid, cancelled]
        contact_id:
          type: integer
          format: int64
        bill_date:
          type: string
          format: date
        due_date:
          type: string
          format: date
          nullable: true
        total_paise:
          type: integer
          format: int64
        balance_paise:
          type: integer
          format: int64
        created_at:
          type: string
          format: date-time

    Payment:
      type: object
      properties:
        id:
          type: integer
          format: int64
        contact_id:
          type: integer
          format: int64
        payment_date:
          type: string
          format: date
        amount_paise:
          type: integer
          format: int64
        reference:
          type: string
          nullable: true
        status:
          type: string
          enum: [draft, posted, voided]
        created_at:
          type: string
          format: date-time

    Receipt:
      type: object
      properties:
        id:
          type: integer
          format: int64
        contact_id:
          type: integer
          format: int64
        receipt_date:
          type: string
          format: date
        amount_paise:
          type: integer
          format: int64
        reference:
          type: string
          nullable: true
        status:
          type: string
          enum: [draft, posted, voided]
        created_at:
          type: string
          format: date-time

    WebhookSubscription:
      type: object
      properties:
        id:
          type: integer
          format: int64
        provider:
          type: string
          enum: [slack, teams]
        category:
          type: string
          description: Event type to subscribe to, or "*" for all events
          example: employee.created
        url_masked:
          type: string
          description: Webhook URL masked for security (full URL not returned after creation)
        label:
          type: string
        is_active:
          type: boolean
        last_used_at:
          type: string
          format: date-time
          nullable: true
        last_error:
          type: string
          nullable: true
        last_status:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time

    WebhookDelivery:
      type: object
      properties:
        id:
          type: integer
          format: int64
        notif_category:
          type: string
        payload_summary:
          type: string
        status_code:
          type: integer
          nullable: true
        error:
          type: string
          nullable: true
        sent_at:
          type: string
          format: date-time

  responses:
    Unauthorized:
      description: Missing or invalid bearer token
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Token lacks required capability or feature not enabled
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    TooManyRequests:
      description: Rate limit exceeded (60 req/min per token)
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

paths:
  # ── Employees ────────────────────────────────────────────────────────────

  /api/v1/public/employees:
    get:
      summary: List employees
      operationId: listEmployees
      tags: [People]
      description: |
        Returns the full employee roster for the token's tenant. The roster
        includes sensitive PII (bank/PAN/salary/nominee), so this endpoint
        requires an HR-admin identity: the token's owner must hold the
        company_admin or hr_manager role, and the token must have the `read`
        capability. A token owned by any other role receives 403.
      responses:
        "200":
          description: List of employees
          content:
            application/json:
              schema:
                type: object
                properties:
                  employees:
                    type: array
                    items:
                      $ref: "#/components/schemas/Employee"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  # ── Leave ─────────────────────────────────────────────────────────────────

  /api/v1/public/leave/requests:
    get:
      summary: List leave requests
      operationId: listLeaveRequests
      tags: [Leave]
      description: |
        Results are role-filtered: employee tokens see own requests; manager
        tokens see own + direct reports; hr_manager/company_admin tokens see
        all tenant requests.
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, approved, rejected, cancelled]
        - name: scope
          in: query
          schema:
            type: string
            enum: [self]
          description: Pass "self" to narrow to the token owner's requests only
      responses:
        "200":
          description: List of leave requests
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/LeaveRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  # ── Expenses ──────────────────────────────────────────────────────────────

  /api/v1/public/expenses:
    get:
      summary: List expenses
      operationId: listExpenses
      tags: [Expenses]
      description: Role-filtered (same rules as leave requests).
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [draft, submitted, approved, rejected, reimbursed]
      responses:
        "200":
          description: List of expenses
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Expense"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  # ── Invoices ──────────────────────────────────────────────────────────────

  /api/v1/public/invoices:
    get:
      summary: List invoices
      operationId: listInvoices
      tags: [Accounting]
      parameters:
        - name: status
          in: query
          schema:
            type: string
        - name: contact_id
          in: query
          schema:
            type: integer
            format: int64
      responses:
        "200":
          description: List of invoices
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Invoice"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/v1/public/invoices/{id}:
    get:
      summary: Get invoice by ID
      operationId: getInvoice
      tags: [Accounting]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        "200":
          description: Invoice detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Invoice"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  # ── Bills ─────────────────────────────────────────────────────────────────

  /api/v1/public/bills:
    get:
      summary: List bills
      operationId: listBills
      tags: [Accounting]
      responses:
        "200":
          description: List of bills
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Bill"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/v1/public/bills/{id}:
    get:
      summary: Get bill by ID
      operationId: getBill
      tags: [Accounting]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        "200":
          description: Bill detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Bill"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  # ── Payments ──────────────────────────────────────────────────────────────

  /api/v1/public/payments:
    get:
      summary: List payments
      operationId: listPayments
      tags: [Accounting]
      responses:
        "200":
          description: List of payments
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Payment"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/v1/public/payments/{id}:
    get:
      summary: Get payment by ID
      operationId: getPayment
      tags: [Accounting]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        "200":
          description: Payment detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Payment"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Receipts ──────────────────────────────────────────────────────────────

  /api/v1/public/receipts:
    get:
      summary: List receipts
      operationId: listReceipts
      tags: [Accounting]
      responses:
        "200":
          description: List of receipts
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Receipt"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/public/receipts/{id}:
    get:
      summary: Get receipt by ID
      operationId: getReceipt
      tags: [Accounting]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        "200":
          description: Receipt detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Receipt"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Webhook subscriptions ─────────────────────────────────────────────────

  /api/v1/public/webhooks:
    get:
      summary: List webhook subscriptions
      operationId: listWebhooks
      tags: [Webhooks]
      responses:
        "200":
          description: List of webhook subscriptions for this tenant
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookSubscription"
        "401":
          $ref: "#/components/responses/Unauthorized"

    post:
      summary: Create a webhook subscription
      operationId: createWebhook
      tags: [Webhooks]
      description: Requires `write` capability.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [provider, category, url, label]
              properties:
                provider:
                  type: string
                  enum: [slack, teams]
                category:
                  type: string
                  description: Event type or "*" for all events
                  example: employee.created
                url:
                  type: string
                  format: uri
                  description: Slack/Teams incoming webhook URL
                label:
                  type: string
                  maxLength: 100
                is_active:
                  type: boolean
                  default: true
      responses:
        "201":
          description: Webhook created (URL masked in response)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookSubscription"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          description: Duplicate webhook (same provider + category + url)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/v1/public/webhooks/{id}:
    get:
      summary: Get webhook subscription
      operationId: getWebhook
      tags: [Webhooks]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        "200":
          description: Webhook detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookSubscription"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

    patch:
      summary: Update a webhook subscription
      operationId: updateWebhook
      tags: [Webhooks]
      description: |
        Updates label, category, or is_active. The URL is immutable after
        creation — delete and recreate to change it.
        Requires `write` capability.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                label:
                  type: string
                  maxLength: 100
                category:
                  type: string
                is_active:
                  type: boolean
      responses:
        "200":
          description: Updated webhook
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookSubscription"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

    delete:
      summary: Delete a webhook subscription
      operationId: deleteWebhook
      tags: [Webhooks]
      description: Requires `write` capability.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        "204":
          description: Deleted
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/public/webhooks/{id}/deliveries:
    get:
      summary: List recent webhook deliveries
      operationId: listWebhookDeliveries
      tags: [Webhooks]
      description: Returns the 50 most recent delivery attempts for a webhook, newest first.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        "200":
          description: Delivery log
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookDelivery"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
