openapi: 3.0.3
info:
  title: QuickBilling API
  description: |
    REST API for QuickBilling — invoice, get paid, and track every expense.
    Create invoices, manage customers, share invoice links, log business expenses,
    set budgets, and query your full P&L programmatically.

    **Authentication**: Use API keys for programmatic access (Unlimited plan).
    Generate keys in Workspace Settings → API Keys. Include in the `Authorization`
    header as `Bearer sk_live_...` or `Bearer sk_test_...`.

    **Workspace context**: Most endpoints require workspace context. Pass
    `X-Workspace-Id` header with your workspace UUID, or use an API key (which
    is workspace-scoped).

    **Expense endpoints**: `/expenses`, `/categories`, `/budgets`, and
    `/overview/workspace/financial*` are documented in this spec under the
    Expenses, Expense Budgets, and Workspace Financials tags.

    **Additional endpoints** (see backend README): templates, color-palettes,
    storage, subscriptions, account, contact, newsletter.
  version: 1.0.0
  contact:
    name: QuickBilling Support
    email: info@quickbilling.io

servers:
  - url: https://app.quickbilling.io/api
    description: Production (app host; `/api` proxied to API)
  - url: http://localhost:3001/api
    description: Local development

security:
  - bearerAuth: []

tags:
  - name: Authentication
    description: API key usage and auth
  - name: Invoices
    description: Create, manage, and share invoices
  - name: Customers
    description: Customer management
  - name: Profiles
    description: Billing profiles
  - name: Email
    description: Send invoices via email
  - name: Expenses
    description: Log, categorise, and manage business expenses
  - name: Expense Budgets
    description: Set and track spend budgets per category or period
  - name: Workspace Financials
    description: P&L, cash flow, and customer profitability reports

paths:
  # ==================== INVOICES ====================
  /invoices:
    get:
      tags: [Invoices]
      summary: List all invoices
      description: Returns all invoices for the current workspace.
      operationId: listInvoices
      responses:
        '200':
          description: List of invoices
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Invoice'
    post:
      tags: [Invoices]
      summary: Create or update invoice
      description: Creates a new invoice or updates an existing one. Include `id` in body to update.
      operationId: createOrUpdateInvoice
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceInput'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true

  /invoices/new:
    post:
      tags: [Invoices]
      summary: Create draft invoice
      description: Creates a new draft invoice and returns its ID.
      operationId: createDraftInvoice
      responses:
        '201':
          description: Draft invoice created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid

  /invoices/new/{templateRef}:
    post:
      tags: [Invoices]
      summary: Create draft from template
      description: Creates a new draft invoice from a template.
      operationId: createFromTemplate
      parameters:
        - $ref: '#/components/parameters/templateRef'
      responses:
        '201':
          description: Draft invoice created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid

  /invoices/{id}:
    get:
      tags: [Invoices]
      summary: Get invoice
      description: Returns a single invoice by ID.
      operationId: getInvoice
      parameters:
        - $ref: '#/components/parameters/invoiceId'
      responses:
        '200':
          description: Invoice details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Invoice'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Invoices]
      summary: Delete invoice
      operationId: deleteInvoice
      parameters:
        - $ref: '#/components/parameters/invoiceId'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '404':
          $ref: '#/components/responses/NotFound'

  /invoices/{id}/activity:
    get:
      tags: [Invoices]
      summary: Get invoice activity
      description: Returns activity log for the invoice.
      operationId: getInvoiceActivity
      parameters:
        - $ref: '#/components/parameters/invoiceId'
      responses:
        '200':
          description: Activity log
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ActivityEntry'

  /invoices/{id}/analytics:
    get:
      tags: [Invoices]
      summary: Get invoice analytics
      description: Returns analytics data for the invoice.
      operationId: getInvoiceAnalytics
      parameters:
        - $ref: '#/components/parameters/invoiceId'
      responses:
        '200':
          description: Analytics data

  /invoices/{id}/share:
    post:
      tags: [Invoices]
      summary: Generate share link
      description: Generates a shareable link for the invoice. Subject to plan limits.
      operationId: generateShareLink
      parameters:
        - $ref: '#/components/parameters/invoiceId'
      responses:
        '200':
          description: Share link generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  shareToken:
                    type: string
                    format: uuid
                  shareUrl:
                    type: string
                    format: uri
        '403':
          description: Limit exceeded (upgrade required)
    delete:
      tags: [Invoices]
      summary: Revoke share link
      operationId: revokeShareLink
      parameters:
        - $ref: '#/components/parameters/invoiceId'
      responses:
        '200':
          description: Share link revoked

  /invoices/{id}/status:
    patch:
      tags: [Invoices]
      summary: Update invoice status
      operationId: updateInvoiceStatus
      parameters:
        - $ref: '#/components/parameters/invoiceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status:
                  type: string
                  enum: [Draft, Ready, Sent, Paid, Overdue, Void]
      responses:
        '200':
          description: Status updated

  # ==================== CUSTOMERS ====================
  /customers:
    get:
      tags: [Customers]
      summary: List all customers
      operationId: listCustomers
      responses:
        '200':
          description: List of customers
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Customer'
    post:
      tags: [Customers]
      summary: Create or update customer
      description: Include `id` to update existing customer.
      operationId: createOrUpdateCustomer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerInput'
      responses:
        '200':
          description: Success

  /customers/{id}:
    delete:
      tags: [Customers]
      summary: Delete customer
      operationId: deleteCustomer
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Customer deleted

  # ==================== PROFILES ====================
  /profiles:
    get:
      tags: [Profiles]
      summary: List all billing profiles
      operationId: listProfiles
      responses:
        '200':
          description: List of profiles
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/BillingProfile'
    post:
      tags: [Profiles]
      summary: Create or update billing profile
      operationId: createOrUpdateProfile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BillingProfileInput'
      responses:
        '200':
          description: Success

  /profiles/{id}:
    get:
      tags: [Profiles]
      summary: Get billing profile
      operationId: getProfile
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Profile details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BillingProfile'
    delete:
      tags: [Profiles]
      summary: Delete billing profile
      operationId: deleteProfile
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Profile deleted

  # ==================== EMAIL ====================
  /send-invoice:
    post:
      tags: [Email]
      summary: Send invoice via email
      description: Sends the invoice to specified recipients.
      operationId: sendInvoice
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendInvoiceInput'
      responses:
        '200':
          description: Email sent successfully
        '400':
          description: Validation error

  # ==================== AUTH - API KEYS ====================
  /auth/workspaces/{workspaceId}/api-keys:
    get:
      tags: [Authentication]
      summary: List API keys
      description: Lists API keys for the workspace. Requires JWT (not API key).
      operationId: listApiKeys
      parameters:
        - $ref: '#/components/parameters/workspaceId'
      responses:
        '200':
          description: List of API keys (prefix only, never full key)
    post:
      tags: [Authentication]
      summary: Generate API key
      description: Creates a new API key. The full key is returned only once.
      operationId: generateApiKey
      parameters:
        - $ref: '#/components/parameters/workspaceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  maxLength: 50
      responses:
        '201':
          description: API key created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  name:
                    type: string
                  key:
                    type: string
                    description: Full key - only returned on creation
                  prefix:
                    type: string
                  createdAt:
                    type: string
                    format: date-time
  /auth/workspaces/{workspaceId}/api-keys/{keyId}:
    delete:
      tags: [Authentication]
      summary: Revoke API key
      operationId: revokeApiKey
      parameters:
        - $ref: '#/components/parameters/workspaceId'
        - name: keyId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: API key revoked

  # ─── Expense paths (appended) ────────────────────────────────────────────────

  /expenses:
    get:
      tags: [Expenses]
      summary: List expenses
      description: Returns expenses for the current workspace. Supports filtering by status, category, date range, billable flag, and vendor.
      operationId: listExpenses
      parameters:
        - in: query
          name: status
          schema: { type: string }
          description: Filter by expense status (draft, pending, approved, paid, rejected, void)
        - in: query
          name: categoryId
          schema: { type: string, format: uuid }
        - in: query
          name: dateFrom
          schema: { type: string, format: date }
        - in: query
          name: dateTo
          schema: { type: string, format: date }
        - in: query
          name: billable
          schema: { type: boolean }
      responses:
        '200':
          description: List of expenses
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Expense' }
    post:
      tags: [Expenses]
      summary: Create expense
      operationId: createExpense
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Expense' }
      responses:
        '201':
          description: Created expense
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Expense' }

  /expenses/stats:
    get:
      tags: [Expenses]
      summary: Expense statistics
      description: Returns aggregated totals and status breakdown for the workspace.
      operationId: getExpenseStats
      parameters:
        - in: query
          name: dateFrom
          schema: { type: string, format: date }
        - in: query
          name: dateTo
          schema: { type: string, format: date }
      responses:
        '200':
          description: Aggregated expense stats
          content:
            application/json:
              schema:
                type: object
                properties:
                  total: { type: number }
                  targetCurrency: { type: string }
                  count: { type: integer }
                  byStatus: { type: object }

  /expenses/reports/summary:
    get:
      tags: [Expenses]
      summary: Expense report summary
      description: KPIs for a date range — total spend, count, billable, and tax-deductible totals.
      operationId: getExpenseReportSummary
      parameters:
        - in: query
          name: start
          required: true
          schema: { type: string, format: date }
        - in: query
          name: end
          required: true
          schema: { type: string, format: date }
      responses:
        '200':
          description: Report summary

  /expenses/reports/trends:
    get:
      tags: [Expenses]
      summary: Monthly spend trends
      description: Returns spend totals grouped by month for charting.
      operationId: getExpenseTrends
      parameters:
        - in: query
          name: start
          required: true
          schema: { type: string, format: date }
        - in: query
          name: end
          required: true
          schema: { type: string, format: date }
      responses:
        '200':
          description: Array of monthly totals

  /expenses/reports/cost-analysis:
    get:
      tags: [Expenses]
      summary: Cost analysis
      description: Break down spend by category, vendor, or payment method.
      operationId: getCostAnalysis
      parameters:
        - in: query
          name: start
          required: true
          schema: { type: string, format: date }
        - in: query
          name: end
          required: true
          schema: { type: string, format: date }
        - in: query
          name: groupBy
          required: true
          schema: { type: string, enum: [category, customer, vendor, month, paymentMethod] }
      responses:
        '200':
          description: Cost analysis rows

  /expenses/reports/tax-deductible:
    get:
      tags: [Expenses]
      summary: Tax-deductible summary
      description: Yearly summary of tax-deductible expenses grouped by category.
      operationId: getTaxDeductibleReport
      parameters:
        - in: query
          name: year
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Tax deductible summary

  /expenses/vendors:
    get:
      tags: [Expenses]
      summary: List expense vendors
      operationId: listExpenseVendors
      responses:
        '200':
          description: Vendors for workspace
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ExpenseVendor' }
    post:
      tags: [Expenses]
      summary: Create expense vendor
      operationId: createExpenseVendor
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, maxLength: 200 }
                email: { type: string, format: email, nullable: true }
                notes: { type: string, maxLength: 2000, nullable: true }
      responses:
        '201':
          description: Created vendor
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExpenseVendor' }

  /expenses/vendors/{id}:
    put:
      tags: [Expenses]
      summary: Update expense vendor
      operationId: updateExpenseVendor
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, maxLength: 200 }
                email: { type: string, format: email, nullable: true }
                notes: { type: string, maxLength: 2000, nullable: true }
      responses:
        '200':
          description: Updated vendor
    delete:
      tags: [Expenses]
      summary: Delete expense vendor
      operationId: deleteExpenseVendor
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Deleted

  /expenses/tags:
    get:
      tags: [Expenses]
      summary: List expense tags
      operationId: listExpenseTags
      responses:
        '200':
          description: Tag definitions for workspace
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ExpenseTag' }
    post:
      tags: [Expenses]
      summary: Create expense tag
      operationId: createExpenseTag
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, maxLength: 100 }
      responses:
        '201':
          description: Created tag
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExpenseTag' }

  /expenses/tags/{id}:
    put:
      tags: [Expenses]
      summary: Update expense tag
      operationId: updateExpenseTag
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, maxLength: 100 }
      responses:
        '200':
          description: Updated tag
    delete:
      tags: [Expenses]
      summary: Delete expense tag
      operationId: deleteExpenseTag
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Deleted

  /expenses/{id}:
    get:
      tags: [Expenses]
      summary: Get expense
      operationId: getExpense
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Expense object
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Expense' }
    patch:
      tags: [Expenses]
      summary: Update expense
      operationId: updateExpense
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Expense' }
      responses:
        '200':
          description: Updated expense
    delete:
      tags: [Expenses]
      summary: Delete expense
      operationId: deleteExpense
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '204':
          description: Deleted

  /categories:
    get:
      tags: [Expenses]
      summary: List expense categories
      operationId: listCategories
      responses:
        '200':
          description: List of categories
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ExpenseCategory' }
    post:
      tags: [Expenses]
      summary: Create category
      operationId: createCategory
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ExpenseCategory' }
      responses:
        '201':
          description: Created category

  /budgets:
    get:
      tags: [Expense Budgets]
      summary: List budgets
      operationId: listBudgets
      responses:
        '200':
          description: List of budgets
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ExpenseBudget' }
    post:
      tags: [Expense Budgets]
      summary: Create budget
      operationId: createBudget
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ExpenseBudget' }
      responses:
        '201':
          description: Created budget

  /budgets/spend-vs-budget:
    get:
      tags: [Expense Budgets]
      summary: Spend vs budget
      description: Returns actual spend compared to budgeted amount for each active budget in the given period.
      operationId: getSpendVsBudget
      parameters:
        - in: query
          name: start
          required: true
          schema: { type: string, format: date }
        - in: query
          name: end
          required: true
          schema: { type: string, format: date }
      responses:
        '200':
          description: Array of budget rows with actual and budgeted totals

  /budgets/{id}:
    put:
      tags: [Expense Budgets]
      summary: Update budget
      operationId: updateBudget
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ExpenseBudget' }
      responses:
        '200':
          description: Updated budget
    delete:
      tags: [Expense Budgets]
      summary: Delete budget
      operationId: deleteBudget
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '204':
          description: Deleted

  /overview/workspace/financial:
    get:
      tags: [Workspace Financials]
      summary: Financial P&L report
      description: Combined revenue (from invoices) and expenses P&L for a date range, with monthly breakdown.
      operationId: getFinancialPnl
      parameters:
        - in: query
          name: start
          required: true
          schema: { type: string, format: date }
        - in: query
          name: end
          required: true
          schema: { type: string, format: date }
      responses:
        '200':
          description: P&L report with revenue, expenses, monthly breakdown, and target currency

  /overview/workspace/financial/cash-flow:
    get:
      tags: [Workspace Financials]
      summary: Cash flow
      description: Monthly cash-in (invoices paid) and cash-out (expenses) with running cumulative net.
      operationId: getFinancialCashFlow
      parameters:
        - in: query
          name: start
          required: true
          schema: { type: string, format: date }
        - in: query
          name: end
          required: true
          schema: { type: string, format: date }
      responses:
        '200':
          description: Cash flow series

  /overview/workspace/financial/by-customer:
    get:
      tags: [Workspace Financials]
      summary: Revenue and expenses by customer
      description: Per-customer P&L with revenue, expenses, net income, and margin percentage.
      operationId: getFinancialByCustomer
      parameters:
        - in: query
          name: start
          required: true
          schema: { type: string, format: date }
        - in: query
          name: end
          required: true
          schema: { type: string, format: date }
      responses:
        '200':
          description: Array of customer P&L rows

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: Use your API key (sk_live_... or sk_test_...) or JWT token

  parameters:
    invoiceId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    templateRef:
      name: templateRef
      in: path
      required: true
      schema:
        type: string
    workspaceId:
      name: workspaceId
      in: path
      required: true
      schema:
        type: string
        format: uuid

  responses:
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string

  schemas:
    Invoice:
      type: object
      properties:
        id: { type: string, format: uuid }
        number: { type: string }
        status: { type: string, enum: [Draft, Ready, Sent, Paid, Overdue, Void] }
        profileId: { type: string, format: uuid, nullable: true }
        customerId: { type: string, format: uuid, nullable: true }
        issueDate: { type: string, format: date }
        dueDate: { type: string, format: date }
        currency: { type: string }
        subtotal: { type: number }
        taxTotal: { type: number }
        total: { type: number }
        items: { type: array, items: { $ref: '#/components/schemas/LineItem' } }

    InvoiceInput:
      type: object
      required: [id, number, issueDate, dueDate]
      properties:
        id: { type: string, format: uuid }
        number: { type: string, maxLength: 50 }
        profileId: { type: string, format: uuid, nullable: true }
        customerId: { type: string, format: uuid, nullable: true }
        customerDetails:
          type: object
          properties:
            name: { type: string }
            email: { type: string }
            address: { type: string }
            taxId: { type: string }
        status: { type: string, enum: [Draft, Ready, Sent, Paid, Overdue, Void], default: Draft }
        issueDate: { type: string, format: date }
        dueDate: { type: string, format: date }
        currency: { type: string, default: USD }
        notes: { type: string, maxLength: 1000, nullable: true }
        template: { type: string, enum: [minimal, modern, corporate], default: minimal }
        items:
          type: array
          items:
            $ref: '#/components/schemas/LineItemInput'

    LineItem:
      type: object
      properties:
        id: { type: string }
        description: { type: string }
        quantity: { type: number }
        unit: { type: string }
        price: { type: number }
        taxRate: { type: number }

    LineItemInput:
      type: object
      required: [id, quantity, price]
      properties:
        id: { type: string }
        description: { type: string, default: '' }
        quantity: { type: number }
        unit: { type: string, default: pcs }
        price: { type: number }
        taxRate: { type: number, default: 0 }

    Customer:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        email: { type: string, nullable: true }
        address: { type: string, nullable: true }
        street: { type: string, nullable: true }
        city: { type: string, nullable: true }
        postalCode: { type: string, nullable: true }
        country: { type: string, nullable: true }
        taxId: { type: string, nullable: true }

    CustomerInput:
      type: object
      required: [name]
      properties:
        id: { type: string, format: uuid }
        name: { type: string, maxLength: 255 }
        email: { type: string, nullable: true }
        address: { type: string, maxLength: 500, nullable: true }
        street: { type: string, maxLength: 255, nullable: true }
        city: { type: string, maxLength: 120, nullable: true }
        postalCode: { type: string, maxLength: 30, nullable: true }
        country: { type: string, maxLength: 120, nullable: true }
        taxId: { type: string, maxLength: 50, nullable: true }

    BillingProfile:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        legalName: { type: string }
        email: { type: string, nullable: true }
        currency: { type: string }
        paymentTerms: { type: integer }

    BillingProfileInput:
      type: object
      required: [id, name, legalName]
      properties:
        id: { type: string }
        name: { type: string, maxLength: 255 }
        legalName: { type: string, maxLength: 255 }
        email: { type: string, nullable: true }
        address: { type: string, maxLength: 500, nullable: true }
        street: { type: string, maxLength: 255, nullable: true }
        city: { type: string, maxLength: 120, nullable: true }
        postalCode: { type: string, maxLength: 30, nullable: true }
        country: { type: string, maxLength: 120, nullable: true }
        taxId: { type: string, maxLength: 50, nullable: true }
        currency: { type: string, default: USD }
        paymentTerms: { type: integer, default: 14 }

    SendInvoiceInput:
      type: object
      required: [invoiceId, to, subject, message]
      properties:
        invoiceId: { type: string, format: uuid }
        to: { type: array, items: { type: string, format: email }, minItems: 1 }
        cc: { type: array, items: { type: string, format: email } }
        bcc: { type: array, items: { type: string, format: email } }
        subject: { type: string, maxLength: 255 }
        message: { type: string, maxLength: 2000 }

    ActivityEntry:
      type: object
      properties:
        id: { type: string }
        type: { type: string }
        createdAt: { type: string, format: date-time }
        description: { type: string }

    # ─── Expense schemas ───────────────────────────────────────────────────────
    Expense:
      type: object
      properties:
        id: { type: string, format: uuid }
        workspaceId: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        amount: { type: number }
        currency: { type: string, example: USD }
        customerId: { type: string, format: uuid, nullable: true }
        categoryId: { type: string, format: uuid, nullable: true }
        vendorId: { type: string, format: uuid, nullable: true }
        vendor: { type: string, nullable: true }
        tagIds: { type: array, items: { type: string, format: uuid } }
        tags: { type: array, items: { type: string } }
        paymentMethod: { type: string, nullable: true }
        taxRate: { type: number }
        taxAmount: { type: number, nullable: true }
        status: { type: string, enum: [Draft, Submitted, Approved, Rejected, Paid, Void] }
        isTaxDeductible: { type: boolean }
        isBillable: { type: boolean }
        incurredDate: { type: string, format: date }
        dueDate: { type: string, format: date, nullable: true }
        notes: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    ExpenseVendor:
      type: object
      properties:
        id: { type: string, format: uuid }
        workspaceId: { type: string, format: uuid }
        name: { type: string }
        email: { type: string, format: email, nullable: true }
        notes: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    ExpenseTag:
      type: object
      properties:
        id: { type: string, format: uuid }
        workspaceId: { type: string, format: uuid }
        name: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    ExpenseCategory:
      type: object
      properties:
        id: { type: string, format: uuid }
        workspaceId: { type: string, format: uuid }
        name: { type: string }
        color: { type: string, nullable: true }
        description: { type: string, nullable: true }

    ExpenseBudget:
      type: object
      properties:
        id: { type: string, format: uuid }
        workspaceId: { type: string, format: uuid }
        name: { type: string }
        scopeType: { type: string, enum: [workspace, category, customer, vendor] }
        scopeId: { type: string, nullable: true }
        periodType: { type: string, enum: [monthly, quarterly, yearly, custom] }
        periodStart: { type: string, format: date }
        periodEnd: { type: string, format: date, nullable: true }
        amount: { type: number }
        currency: { type: string }
        alertThresholdPct: { type: number, minimum: 0, maximum: 100 }
