openapi: 3.0.3
info:
  title: E-Factor CRM API
  version: 1.0.0
  description: >
    REST API for the E-Factor CRM / Lead Management module.


    Built on **Laravel 12** and authenticated with **Laravel Sanctum**
    personal-access (bearer) tokens. Every endpoint below lives under the
    `/crm` route group, which is protected by `auth:sanctum`, an `audit`
    middleware (records every write), and `throttle:120,1` (max 120
    requests/min/user).


    ### Authorization

    Access is governed by RBAC modules seeded by `CrmSeeder`:
    `crm_dashboard`, `crm_leads`, `crm_tenders`, `crm_masters`,
    `crm_reports`. The `perm:{module},{action}` middleware checks per-action
    rights; the `admin` role bypasses all checks. The required permission for
    each operation is noted in its description (e.g. "Requires
    `crm_leads:create`").


    ### Response envelope

    All JSON responses follow a common envelope:

    - Success: `{ "success": true, "data": ..., "message"?: "...", "meta"?: {...} }`

    - Error:   `{ "success": false, "message": "...", "errors"?: {...} }`


    Laravel paginators are returned inside `data` with their standard
    `data`/`current_page`/`per_page`/`total`/`last_page` keys.
servers:
  - url: https://efector.hostingaipl.xyz/public/api
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Dashboard
    description: CRM dashboard KPIs
  - name: Leads
    description: Lead lifecycle - list, create, update, stage, revisions, activities
  - name: Proposals
    description: Quotations / proposals against a lead (S2)
  - name: Contacts
    description: Contact people attached to a lead (S3)
  - name: Tenders
    description: Tender workflow - details, EMD/BG, documents, eligibility, go/no-go (S4)
  - name: Reports
    description: CRM analytics reports (S5)
  - name: Masters
    description: CRM master data (sources, statuses, categories, doc types, etc.)
  - name: Import
    description: Bulk CSV lead import (S6)
  - name: Conversion
    description: Convert a lead to party / project / receivable (S7)
  - name: Audit
    description: Read-only audit trail
paths:
  # ─────────────────────────── DASHBOARD ───────────────────────────
  /crm/leads/dashboard:
    get:
      tags: [Dashboard]
      summary: CRM dashboard KPIs
      description: >
        Aggregated KPIs, pipeline-by-stage, upcoming bid dates (next 30 days),
        overdue follow-ups and a 6-month trend. Requires `crm_dashboard:view`.
      operationId: leadsDashboard
      responses:
        '200':
          description: Dashboard payload
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/DashboardData'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  # ─────────────────────────── AUDIT ───────────────────────────────
  /crm/audit-log:
    get:
      tags: [Audit]
      summary: List audit-trail entries
      description: >
        Paginated, read-only audit trail. Requires `user_management:view`.
      operationId: crmAuditLog
      parameters:
        - { name: user_id, in: query, schema: { type: integer }, description: Filter by acting user }
        - { name: module, in: query, schema: { type: string }, description: Prefix match on module }
        - { name: method, in: query, schema: { type: string, enum: [GET, POST, PUT, PATCH, DELETE] } }
        - { name: entity, in: query, schema: { type: string } }
        - { name: q, in: query, schema: { type: string }, description: Substring match on request path }
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Paginated audit log
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/AuditLogPage'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  # ─────────────────────────── MASTERS ─────────────────────────────
  /crm/masters:
    get:
      tags: [Masters]
      summary: All CRM master data (dropdowns)
      description: >
        Returns all active master lists in one call - sources, categories,
        statuses, tender categories, document types, EMD/BG types and
        eligibility criteria. Readable by any CRM user; requires
        `crm_leads:view`.
      operationId: crmMastersIndex
      responses:
        '200':
          description: Master data bundle
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/MastersBundle'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /crm/masters/lead-categories:
    get:
      tags: [Masters]
      summary: List lead categories
      description: Active lead categories, for dropdowns. Requires `crm_leads:view`.
      operationId: leadCategoriesIndex
      responses:
        '200':
          description: Lead category list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/LeadCategory' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Masters]
      summary: Create lead category
      description: Requires `crm_masters:edit`.
      operationId: leadCategoriesStore
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LeadCategoryInput' }
      responses:
        '201': { $ref: '#/components/responses/LeadCategoryResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /crm/masters/lead-categories/{id}:
    parameters:
      - $ref: '#/components/parameters/PathId'
    put:
      tags: [Masters]
      summary: Update lead category
      description: Requires `crm_masters:edit`.
      operationId: leadCategoriesUpdate
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LeadCategoryInput' }
      responses:
        '200': { $ref: '#/components/responses/LeadCategoryResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      tags: [Masters]
      summary: Archive lead category
      description: Soft-deletes (sets is_active=0). Requires `crm_masters:edit`.
      operationId: leadCategoriesDestroy
      responses:
        '200': { $ref: '#/components/responses/MessageOnly' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /crm/masters/sources:
    post:
      tags: [Masters]
      summary: Create lead source
      description: Requires `crm_masters:edit`.
      operationId: mastersStoreSource
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, code]
              properties:
                name: { type: string }
                code: { type: string, description: Unique }
                color: { type: string }
                sort_order: { type: integer }
      responses:
        '201': { $ref: '#/components/responses/MasterItemResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /crm/masters/sources/{id}:
    parameters:
      - $ref: '#/components/parameters/PathId'
    put:
      tags: [Masters]
      summary: Update lead source
      description: Requires `crm_masters:edit`.
      operationId: mastersUpdateSource
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LeadSource' }
      responses:
        '200': { $ref: '#/components/responses/MasterItemResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    delete:
      tags: [Masters]
      summary: Delete lead source
      description: Requires `crm_masters:edit`.
      operationId: mastersDeleteSource
      responses:
        '200': { $ref: '#/components/responses/MessageOnly' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /crm/masters/statuses:
    post:
      tags: [Masters]
      summary: Create lead status
      description: Requires `crm_masters:edit`.
      operationId: mastersStoreStatus
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, code]
              properties:
                name: { type: string }
                code: { type: string, description: Unique }
                lead_type: { type: string, enum: [open_market, tender, all] }
                color: { type: string }
                stage: { type: string }
                is_won: { type: boolean }
                is_lost: { type: boolean }
                sort_order: { type: integer }
      responses:
        '201': { $ref: '#/components/responses/MasterItemResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /crm/masters/statuses/{id}:
    parameters:
      - $ref: '#/components/parameters/PathId'
    put:
      tags: [Masters]
      summary: Update lead status
      description: Requires `crm_masters:edit`.
      operationId: mastersUpdateStatus
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LeadStatus' }
      responses:
        '200': { $ref: '#/components/responses/MasterItemResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    delete:
      tags: [Masters]
      summary: Delete lead status
      description: Requires `crm_masters:edit`.
      operationId: mastersDeleteStatus
      responses:
        '200': { $ref: '#/components/responses/MessageOnly' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /crm/masters/tender-categories:
    post:
      tags: [Masters]
      summary: Create tender category
      description: Requires `crm_masters:edit`.
      operationId: mastersStoreTenderCategory
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, code, tender_type]
              properties:
                name: { type: string }
                code: { type: string, description: Unique }
                tender_type: { type: string, enum: [works, supply, service, consultancy, turnkey, mixed] }
                description: { type: string }
      responses:
        '201': { $ref: '#/components/responses/MasterItemResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /crm/masters/tender-categories/{id}:
    parameters:
      - $ref: '#/components/parameters/PathId'
    put:
      tags: [Masters]
      summary: Update tender category
      description: Requires `crm_masters:edit`.
      operationId: mastersUpdateTenderCategory
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TenderCategory' }
      responses:
        '200': { $ref: '#/components/responses/MasterItemResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    delete:
      tags: [Masters]
      summary: Delete tender category
      description: Requires `crm_masters:edit`.
      operationId: mastersDeleteTenderCategory
      responses:
        '200': { $ref: '#/components/responses/MessageOnly' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /crm/masters/document-types:
    post:
      tags: [Masters]
      summary: Create document type
      description: Requires `crm_masters:edit`.
      operationId: mastersStoreDocType
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, code, category]
              properties:
                name: { type: string }
                code: { type: string, description: Unique }
                category: { type: string, enum: [eligibility, financial, technical, statutory, experience, other] }
                is_mandatory: { type: boolean }
                sort_order: { type: integer }
                description: { type: string }
      responses:
        '201': { $ref: '#/components/responses/MasterItemResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /crm/masters/document-types/{id}:
    parameters:
      - $ref: '#/components/parameters/PathId'
    put:
      tags: [Masters]
      summary: Update document type
      description: Requires `crm_masters:edit`.
      operationId: mastersUpdateDocType
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DocumentType' }
      responses:
        '200': { $ref: '#/components/responses/MasterItemResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    delete:
      tags: [Masters]
      summary: Delete document type
      description: Requires `crm_masters:edit`.
      operationId: mastersDeleteDocType
      responses:
        '200': { $ref: '#/components/responses/MessageOnly' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /crm/masters/emd-bg-types:
    post:
      tags: [Masters]
      summary: Create EMD/BG type
      description: Requires `crm_masters:edit`.
      operationId: mastersStoreEmdBgType
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, code, type]
              properties:
                name: { type: string }
                code: { type: string, description: Unique }
                type: { type: string, enum: [emd, bg, sd, other] }
                mode: { type: string }
                validity_days: { type: integer }
                notes: { type: string }
      responses:
        '201': { $ref: '#/components/responses/MasterItemResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /crm/masters/emd-bg-types/{id}:
    parameters:
      - $ref: '#/components/parameters/PathId'
    put:
      tags: [Masters]
      summary: Update EMD/BG type
      description: Requires `crm_masters:edit`.
      operationId: mastersUpdateEmdBgType
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EmdBgType' }
      responses:
        '200': { $ref: '#/components/responses/MasterItemResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    delete:
      tags: [Masters]
      summary: Delete EMD/BG type
      description: Requires `crm_masters:edit`.
      operationId: mastersDeleteEmdBgType
      responses:
        '200': { $ref: '#/components/responses/MessageOnly' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /crm/masters/eligibility-criteria:
    post:
      tags: [Masters]
      summary: Create eligibility criterion
      description: Requires `crm_masters:edit`.
      operationId: mastersStoreEligibility
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, code, criteria_type]
              properties:
                name: { type: string }
                code: { type: string, description: Unique }
                criteria_type: { type: string }
                description: { type: string }
                sort_order: { type: integer }
      responses:
        '201': { $ref: '#/components/responses/MasterItemResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /crm/masters/eligibility-criteria/{id}:
    parameters:
      - $ref: '#/components/parameters/PathId'
    put:
      tags: [Masters]
      summary: Update eligibility criterion
      description: Requires `crm_masters:edit`.
      operationId: mastersUpdateEligibility
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EligibilityCriteria' }
      responses:
        '200': { $ref: '#/components/responses/MasterItemResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    delete:
      tags: [Masters]
      summary: Delete eligibility criterion
      description: Requires `crm_masters:edit`.
      operationId: mastersDeleteEligibility
      responses:
        '200': { $ref: '#/components/responses/MessageOnly' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  # ─────────────────────────── LEADS ───────────────────────────────
  /crm/leads:
    get:
      tags: [Leads]
      summary: List leads (paginated)
      description: >
        Unified, filterable, paginated lead list. Requires `crm_leads:view`.
      operationId: leadsIndex
      parameters:
        - { name: lead_type, in: query, schema: { type: string, enum: [open_market, tender] } }
        - { name: category_id, in: query, schema: { type: integer } }
        - { name: stage, in: query, schema: { $ref: '#/components/schemas/LeadStage' } }
        - { name: status_id, in: query, schema: { type: integer } }
        - { name: assigned_to, in: query, schema: { type: integer } }
        - { name: priority, in: query, schema: { $ref: '#/components/schemas/LeadPriority' } }
        - { name: is_won, in: query, schema: { type: boolean } }
        - { name: is_lost, in: query, schema: { type: boolean } }
        - { name: q, in: query, schema: { type: string }, description: Search ref_no / title / client_name }
        - { name: from, in: query, schema: { type: string, format: date }, description: enquiry_date >= }
        - { name: to, in: query, schema: { type: string, format: date }, description: enquiry_date <= }
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Paginated leads
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/LeadPage'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Leads]
      summary: Create lead
      description: >
        Creates a lead. If `client_phone` or `client_email` matches an existing
        active lead and `force_create` is not true, responds **409** with the
        existing lead. Send `force_create=true` to override. For tender leads a
        blank tender-detail record is auto-created. Requires `crm_leads:create`.
      operationId: leadsStore
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LeadCreateInput' }
      responses:
        '201':
          description: Lead created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Lead' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/DuplicateLead' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /crm/leads/pipeline:
    get:
      tags: [Leads]
      summary: Pipeline (Kanban) view
      description: >
        Leads grouped by stage (top 20 each) with per-stage count and value.
        Requires `crm_leads:view`.
      operationId: leadsPipeline
      parameters:
        - { name: lead_type, in: query, schema: { type: string, enum: [open_market, tender] } }
      responses:
        '200':
          description: Pipeline grouped by stage
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties:
                          $ref: '#/components/schemas/PipelineColumn'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /crm/leads/import-template:
    get:
      tags: [Import]
      summary: Download CSV import template
      description: Streams a CSV template with headers and one example row. Requires `crm_leads:view`.
      operationId: leadsImportTemplate
      responses:
        '200':
          description: CSV file
          content:
            text/csv:
              schema: { type: string, format: binary }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /crm/leads/import:
    post:
      tags: [Import]
      summary: Bulk CSV lead import
      description: >
        Uploads a CSV (multipart) and creates leads row-by-row. Rows failing
        validation are skipped and reported. Requires `crm_leads:create`.
      operationId: leadsImport
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: CSV/TXT file (mimes:csv,txt)
      responses:
        '200':
          description: Import summary
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/ImportResult' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /crm/leads/{id}:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Leads]
      summary: Lead detail
      description: >
        Full lead with source, status, category, client, contacts, proposals
        (+items), revisions, activities, tender detail, EMD/BG, documents and
        eligibility. Requires `crm_leads:view`.
      operationId: leadsShow
      responses:
        '200':
          description: Lead detail
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/LeadDetail' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Leads]
      summary: Update lead
      description: Requires `crm_leads:edit`.
      operationId: leadsUpdate
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LeadUpdateInput' }
      responses:
        '200':
          description: Lead updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Lead' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      tags: [Leads]
      summary: Archive lead
      description: >
        Soft-deletes (is_active=0). A won lead cannot be deleted (**422**).
        Requires `crm_leads:delete`.
      operationId: leadsDestroy
      responses:
        '200': { $ref: '#/components/responses/MessageOnly' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /crm/leads/{id}/revise:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Leads]
      summary: Revise lead (value/date/stage with reason)
      description: >
        Records a revision entry and applies the change. Requires `crm_leads:edit`.
      operationId: leadsRevise
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LeadReviseInput' }
      responses:
        '200':
          description: Lead revised
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Lead' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /crm/leads/{id}/stage:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Leads]
      summary: Change lead stage (pipeline move)
      description: >
        Moves the lead through the pipeline. Won/lost/withdrawn transitions set
        the corresponding flags and close date. Requires `crm_leads:edit`.
      operationId: leadsChangeStage
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LeadStageInput' }
      responses:
        '200':
          description: Stage changed
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Lead' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /crm/leads/{id}/activities:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Leads]
      summary: List lead activities
      description: Requires `crm_leads:view`.
      operationId: leadsActivities
      responses:
        '200':
          description: Activity list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/LeadActivity' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Leads]
      summary: Log lead activity
      description: >
        Logs an activity; if `next_action_date` is set it becomes the lead's
        next follow-up date. Requires `crm_leads:edit`.
      operationId: leadsStoreActivity
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LeadActivityInput' }
      responses:
        '201':
          description: Activity logged
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/LeadActivity' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  # ─────────────────────────── PROPOSALS (S2) ──────────────────────
  /crm/leads/{id}/proposals:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Proposals]
      summary: List proposals for a lead
      description: Ordered by version desc, with line items. Requires `crm_leads:view`.
      operationId: proposalsIndex
      responses:
        '200':
          description: Proposal list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Proposal' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Proposals]
      summary: Create proposal (quotation)
      description: >
        Creates a versioned quotation with line items; totals (sub_total, GST,
        total_amount) are computed server-side. Auto-moves the lead from
        new/qualified to proposal. Requires `crm_leads:edit`.
      operationId: proposalsStore
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ProposalInput' }
      responses:
        '201':
          description: Proposal created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Proposal' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /crm/leads/{id}/proposals/{propId}:
    parameters:
      - $ref: '#/components/parameters/PathId'
      - $ref: '#/components/parameters/PropId'
    put:
      tags: [Proposals]
      summary: Update proposal
      description: >
        Replaces header fields and re-syncs line items (existing items deleted
        and re-inserted); totals recomputed. Requires `crm_leads:edit`.
      operationId: proposalsUpdate
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ProposalInput' }
      responses:
        '200':
          description: Proposal updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Proposal' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /crm/leads/{id}/proposals/{propId}/pdf:
    parameters:
      - $ref: '#/components/parameters/PathId'
      - $ref: '#/components/parameters/PropId'
    get:
      tags: [Proposals]
      summary: Download proposal PDF
      description: Renders and downloads the quotation as a PDF. Requires `crm_leads:view`.
      operationId: proposalsPdf
      responses:
        '200':
          description: PDF file
          content:
            application/pdf:
              schema: { type: string, format: binary }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /crm/leads/{id}/proposals/{propId}/convert:
    parameters:
      - $ref: '#/components/parameters/PathId'
      - $ref: '#/components/parameters/PropId'
    post:
      tags: [Proposals]
      summary: Convert proposal to receivable
      description: >
        Creates a finance receivable from the proposal and marks the proposal
        accepted. The lead must have a linked client (party) or responds
        **422**. Requires `crm_leads:edit`.
      operationId: proposalsConvert
      responses:
        '201':
          description: Receivable created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          receivable: { type: object, additionalProperties: true }
                          proposal: { $ref: '#/components/schemas/Proposal' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  # ─────────────────────────── CONTACTS (S3) ───────────────────────
  /crm/leads/{id}/contacts:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [Contacts]
      summary: List lead contacts
      description: Primary contact first, then by name. Requires `crm_leads:view`.
      operationId: contactsIndex
      responses:
        '200':
          description: Contact list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Contact' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Contacts]
      summary: Add lead contact
      description: >
        Adds a contact; if `is_primary` is true, other contacts on the lead are
        demoted. Requires `crm_leads:edit`.
      operationId: contactsStore
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ContactInput' }
      responses:
        '201':
          description: Contact added
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Contact' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /crm/leads/{id}/contacts/{contactId}:
    parameters:
      - $ref: '#/components/parameters/PathId'
      - $ref: '#/components/parameters/ContactId'
    put:
      tags: [Contacts]
      summary: Update lead contact
      description: Requires `crm_leads:edit`.
      operationId: contactsUpdate
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ContactInput' }
      responses:
        '200':
          description: Contact updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Contact' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      tags: [Contacts]
      summary: Delete lead contact
      description: Requires `crm_leads:edit`.
      operationId: contactsDestroy
      responses:
        '200': { $ref: '#/components/responses/MessageOnly' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ─────────────────────────── CONVERSION (S7) ─────────────────────
  /crm/leads/{id}/convert:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Conversion]
      summary: Convert lead to party / project / receivable
      description: >
        Ensures a client party exists, optionally creates a project and/or a
        receivable (from a proposal), and marks the lead won. Requires
        `crm_leads:edit`.
      operationId: leadsConvert
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LeadConvertInput' }
      responses:
        '200':
          description: Lead converted
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/LeadConvertResult' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  # ─────────────────────────── TENDERS (S4) ────────────────────────
  /crm/tender-calendar:
    get:
      tags: [Tenders]
      summary: Tender calendar (bids by submission month)
      description: >
        Tender bid events for the given month, keyed by date. Requires
        `crm_tenders:view`.
      operationId: tenderCalendar
      parameters:
        - { name: month, in: query, schema: { type: string, pattern: '^\d{4}-\d{2}$' }, description: 'YYYY-MM (defaults to current month)' }
      responses:
        '200':
          description: Tender calendar
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/TenderCalendar' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /crm/leads/{id}/tender-detail:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Tenders]
      summary: Save tender detail
      description: >
        Upserts the tender detail for a tender lead. bid_value syncs to the
        lead's approx_value; bid_submission_date syncs to expected_close_date.
        Requires `crm_tenders:edit`.
      operationId: saveTenderDetail
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TenderDetailInput' }
      responses:
        '200':
          description: Tender detail saved
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/TenderDetail' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /crm/leads/{id}/emd-bg:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Tenders]
      summary: Add EMD/BG record
      description: Requires `crm_tenders:edit`.
      operationId: storeEmdBg
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EmdBgRecordInput' }
      responses:
        '201':
          description: EMD/BG record added
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/EmdBgRecord' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /crm/leads/{id}/emd-bg/{recordId}:
    parameters:
      - $ref: '#/components/parameters/PathId'
      - { name: recordId, in: path, required: true, schema: { type: integer } }
    put:
      tags: [Tenders]
      summary: Update EMD/BG record
      description: Requires `crm_tenders:edit`.
      operationId: updateEmdBg
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EmdBgRecordInput' }
      responses:
        '200':
          description: EMD/BG updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/EmdBgRecord' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /crm/leads/{id}/documents:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Tenders]
      summary: Add tender document
      description: Requires `crm_tenders:edit`.
      operationId: storeTenderDoc
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TenderDocumentInput' }
      responses:
        '201':
          description: Document added
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/TenderDocument' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /crm/leads/{id}/documents/{docId}:
    parameters:
      - $ref: '#/components/parameters/PathId'
      - { name: docId, in: path, required: true, schema: { type: integer } }
    put:
      tags: [Tenders]
      summary: Update tender document
      description: Requires `crm_tenders:edit`.
      operationId: updateTenderDoc
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TenderDocumentInput' }
      responses:
        '200':
          description: Document updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/TenderDocument' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /crm/leads/{id}/eligibility:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Tenders]
      summary: Add eligibility check
      description: Requires `crm_tenders:edit`.
      operationId: storeEligibilityCheck
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EligibilityCheckInput' }
      responses:
        '201':
          description: Eligibility check saved
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/TenderEligibility' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /crm/leads/{id}/eligibility/{checkId}:
    parameters:
      - $ref: '#/components/parameters/PathId'
      - { name: checkId, in: path, required: true, schema: { type: integer } }
    put:
      tags: [Tenders]
      summary: Update eligibility check
      description: Requires `crm_tenders:edit`.
      operationId: updateEligibilityCheck
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EligibilityCheckInput' }
      responses:
        '200':
          description: Eligibility check updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/TenderEligibility' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /crm/leads/{id}/init-eligibility:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Tenders]
      summary: Initialise eligibility from masters
      description: >
        Bulk-creates eligibility check rows for the lead from the active
        eligibility-criteria masters (skips already-added). Requires
        `crm_tenders:edit`.
      operationId: initEligibilityFromMasters
      responses:
        '200': { $ref: '#/components/responses/MessageOnly' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /crm/leads/{id}/go-no-go:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Tenders]
      summary: Record Go / No-Go decision
      description: >
        Records a go/no-go decision on a tender lead (bid_status becomes
        go_decision / not_go) plus a revision entry. Only tender leads (else
        **422**). Requires `crm_tenders:edit`.
      operationId: tenderGoNoGo
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [decision, reason]
              properties:
                decision: { type: string, enum: [go, no_go] }
                reason: { type: string, minLength: 5 }
      responses:
        '200':
          description: Decision recorded
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/TenderDetail' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /crm/leads/{id}/convert-to-project:
    parameters:
      - $ref: '#/components/parameters/PathId'
    post:
      tags: [Tenders]
      summary: Convert won tender to project
      description: >
        Creates a project from a tender lead. Only tender leads whose
        tender_detail.bid_status is `work_order` and that have not already been
        converted; otherwise **422**. Requires `crm_tenders:edit`.
      operationId: tenderConvertToProject
      responses:
        '200':
          description: Tender converted to project
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          lead: { $ref: '#/components/schemas/Lead' }
                          project: { type: object, additionalProperties: true }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  # ─────────────────────────── REPORTS (S5) ────────────────────────
  /crm/reports/funnel:
    get:
      tags: [Reports]
      summary: Funnel report
      description: >
        Leads grouped by stage (canonical order) with count, total value and
        average days in stage. Requires `crm_reports:view`.
      operationId: reportFunnel
      parameters:
        - $ref: '#/components/parameters/ReportFrom'
        - $ref: '#/components/parameters/ReportTo'
        - { name: lead_type, in: query, schema: { type: string, enum: [open_market, tender] } }
        - $ref: '#/components/parameters/ReportFormat'
      responses:
        '200': { $ref: '#/components/responses/FunnelReport' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /crm/reports/source-performance:
    get:
      tags: [Reports]
      summary: Source performance report
      description: Leads/won/lost/win-rate per source. Requires `crm_reports:view`.
      operationId: reportSourcePerformance
      parameters:
        - $ref: '#/components/parameters/ReportFrom'
        - $ref: '#/components/parameters/ReportTo'
        - $ref: '#/components/parameters/ReportFormat'
      responses:
        '200':
          description: Source performance rows (or Excel/CSV file)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/SourcePerformanceRow' }
            application/octet-stream:
              schema: { type: string, format: binary }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /crm/reports/salesperson:
    get:
      tags: [Reports]
      summary: Salesperson performance report
      description: Per-user leads/won/lost/win-rate/overdue/activity counts. Requires `crm_reports:view`.
      operationId: reportSalesperson
      parameters:
        - $ref: '#/components/parameters/ReportFrom'
        - $ref: '#/components/parameters/ReportTo'
        - { name: assigned_to, in: query, schema: { type: integer } }
        - $ref: '#/components/parameters/ReportFormat'
      responses:
        '200':
          description: Salesperson rows (or Excel/CSV file)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/SalespersonRow' }
            application/octet-stream:
              schema: { type: string, format: binary }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /crm/reports/activity-log:
    get:
      tags: [Reports]
      summary: Activity log report
      description: >
        Paginated cross-lead activity feed. With `format=excel` returns all
        rows as a file. Requires `crm_reports:view`.
      operationId: reportActivityLog
      parameters:
        - $ref: '#/components/parameters/ReportFrom'
        - $ref: '#/components/parameters/ReportTo'
        - { name: assigned_to, in: query, schema: { type: integer } }
        - { name: activity_type, in: query, schema: { type: string } }
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/ReportFormat'
      responses:
        '200':
          description: Paginated activity rows (or Excel/CSV file)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { type: object, additionalProperties: true, description: Laravel paginator }
            application/octet-stream:
              schema: { type: string, format: binary }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /crm/reports/tender-winrate:
    get:
      tags: [Reports]
      summary: Tender win-rate report
      description: Aggregate tender bid/won/lost/win-rate & average values. Requires `crm_reports:view`.
      operationId: reportTenderWinRate
      parameters:
        - $ref: '#/components/parameters/ReportFrom'
        - $ref: '#/components/parameters/ReportTo'
        - { name: category_id, in: query, schema: { type: integer } }
        - { name: tender_mode, in: query, schema: { type: string } }
        - $ref: '#/components/parameters/ReportFormat'
      responses:
        '200':
          description: Tender win-rate aggregate (or Excel/CSV file)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/TenderWinRate' }
            application/octet-stream:
              schema: { type: string, format: binary }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /crm/reports/pipeline-ageing:
    get:
      tags: [Reports]
      summary: Pipeline ageing report
      description: >
        Open leads with days-in-stage and days-since-last-activity, oldest
        first. Requires `crm_reports:view`.
      operationId: reportPipelineAgeing
      parameters:
        - { name: lead_type, in: query, schema: { type: string, enum: [open_market, tender] } }
        - $ref: '#/components/parameters/ReportFormat'
      responses:
        '200':
          description: Pipeline ageing rows (or Excel/CSV file)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiSuccess'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/PipelineAgeingRow' }
            application/octet-stream:
              schema: { type: string, format: binary }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Laravel Sanctum personal-access token.

  parameters:
    PathId:
      name: id
      in: path
      required: true
      description: Lead ID (or resource ID for masters).
      schema: { type: integer }
    PropId:
      name: propId
      in: path
      required: true
      schema: { type: integer }
    ContactId:
      name: contactId
      in: path
      required: true
      schema: { type: integer }
    PerPage:
      name: per_page
      in: query
      description: Items per page (default varies by endpoint).
      schema: { type: integer, default: 25 }
    ReportFrom:
      name: from
      in: query
      description: Filter enquiry_date >= (YYYY-MM-DD).
      schema: { type: string, format: date }
    ReportTo:
      name: to
      in: query
      description: Filter enquiry_date <= (YYYY-MM-DD).
      schema: { type: string, format: date }
    ReportFormat:
      name: format
      in: query
      description: Set to `excel` to download the report as an .xlsx (or CSV fallback) instead of JSON.
      schema: { type: string, enum: [excel] }

  responses:
    Unauthorized:
      description: Missing or invalid bearer token
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }
          example: { success: false, message: Unauthenticated. }
    Forbidden:
      description: Authenticated but lacks the required permission
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }
          example: { success: false, message: This action is unauthorized. }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }
          example: { success: false, message: Not found. }
    ValidationError:
      description: Validation failed
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }
          example:
            success: false
            message: The given data was invalid.
            errors:
              title: [The title field is required.]
    DuplicateLead:
      description: Possible duplicate lead detected (send force_create=true to override)
      content:
        application/json:
          schema:
            type: object
            properties:
              success: { type: boolean, example: false }
              duplicate: { type: boolean, example: true }
              existing_lead:
                type: object
                properties:
                  id: { type: integer }
                  ref_no: { type: string }
                  title: { type: string }
                  stage: { $ref: '#/components/schemas/LeadStage' }
              message: { type: string, example: 'Possible duplicate lead found. Send force_create=true to proceed.' }
    MessageOnly:
      description: Success with message only
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/ApiSuccess'
              - type: object
                properties:
                  message: { type: string }
    LeadCategoryResponse:
      description: Lead category
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/ApiSuccess'
              - type: object
                properties:
                  data: { $ref: '#/components/schemas/LeadCategory' }
    MasterItemResponse:
      description: Master record
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/ApiSuccess'
              - type: object
                properties:
                  data: { type: object, additionalProperties: true }
    FunnelReport:
      description: Funnel rows (or Excel/CSV file when format=excel)
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/ApiSuccess'
              - type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/ReportFunnelRow' }
        application/octet-stream:
          schema: { type: string, format: binary }

  schemas:
    # ── Envelopes ──────────────────────────────────────────────────
    ApiSuccess:
      type: object
      required: [success]
      properties:
        success: { type: boolean, example: true }
        data: {}
        message: { type: string }
        meta: { type: object, additionalProperties: true }
    ApiError:
      type: object
      required: [success, message]
      properties:
        success: { type: boolean, example: false }
        message: { type: string }
        errors:
          type: object
          additionalProperties:
            type: array
            items: { type: string }

    # ── Enums ──────────────────────────────────────────────────────
    LeadStage:
      type: string
      enum: [new, qualified, proposal, negotiation, won, lost, withdrawn]
    LeadPriority:
      type: string
      enum: [low, medium, high, critical]
    LeadType:
      type: string
      enum: [open_market, tender]
    GstType:
      type: string
      enum: [cgst_sgst, igst, exempt]
    ProposalStatus:
      type: string
      enum: [draft, sent, under_review, accepted, rejected, revised]
    ContactRole:
      type: string
      enum: [decision_maker, influencer, technical, financial, admin, other]
    ActivityOutcome:
      type: string
      enum: [positive, neutral, negative, no_response]
    EmdBgMode:
      type: string
      enum: [dd, bg, online, cheque, fdr, neft, upi]
    EmdBgStatus:
      type: string
      enum: [prepared, submitted, returned, forfeited, converted, cancelled]
    MatchStatus:
      type: string
      enum: [met, not_met, partial, under_review, not_applicable]

    # ── Lead ───────────────────────────────────────────────────────
    Lead:
      type: object
      properties:
        id: { type: integer }
        ref_no: { type: string, description: Auto-generated reference number }
        lead_type: { $ref: '#/components/schemas/LeadType' }
        lead_category_id: { type: integer, nullable: true }
        title: { type: string }
        description: { type: string, nullable: true }
        client_id: { type: integer, nullable: true, description: fin_parties.id }
        client_name: { type: string, nullable: true }
        client_dept: { type: string, nullable: true }
        client_contact: { type: string, nullable: true }
        client_email: { type: string, format: email, nullable: true }
        client_phone: { type: string, nullable: true }
        client_location: { type: string, nullable: true }
        source_id: { type: integer, nullable: true }
        status_id: { type: integer, nullable: true }
        stage: { $ref: '#/components/schemas/LeadStage' }
        priority: { $ref: '#/components/schemas/LeadPriority' }
        probability: { type: integer, minimum: 0, maximum: 100, nullable: true }
        currency: { type: string, nullable: true }
        approx_value: { type: number, format: double, nullable: true }
        revised_value: { type: number, format: double, nullable: true }
        final_value: { type: number, format: double, nullable: true }
        assigned_to: { type: integer, nullable: true }
        team_lead_id: { type: integer, nullable: true }
        department_id: { type: integer, nullable: true }
        enquiry_date: { type: string, format: date, nullable: true }
        expected_close_date: { type: string, format: date, nullable: true }
        actual_close_date: { type: string, format: date, nullable: true }
        next_followup_date: { type: string, format: date, nullable: true }
        is_won: { type: boolean }
        is_lost: { type: boolean }
        is_withdrawn: { type: boolean }
        is_active: { type: boolean }
        win_loss_reason: { type: string, nullable: true }
        lost_to_competitor: { type: string, nullable: true }
        notes: { type: string, nullable: true }
        stage_changed_at: { type: string, format: date-time, nullable: true }
        stage_changed_by: { type: integer, nullable: true }
        project_id: { type: integer, nullable: true }
        created_by: { type: integer }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    LeadDetail:
      allOf:
        - $ref: '#/components/schemas/Lead'
        - type: object
          properties:
            contacts:
              type: array
              items: { $ref: '#/components/schemas/Contact' }
            proposals:
              type: array
              items: { $ref: '#/components/schemas/Proposal' }
            activities:
              type: array
              items: { $ref: '#/components/schemas/LeadActivity' }
            revisions:
              type: array
              items: { $ref: '#/components/schemas/LeadRevision' }
            tenderDetail: { $ref: '#/components/schemas/TenderDetail' }
            emdBgRecords:
              type: array
              items: { $ref: '#/components/schemas/EmdBgRecord' }
            documents:
              type: array
              items: { $ref: '#/components/schemas/TenderDocument' }
            eligibility:
              type: array
              items: { $ref: '#/components/schemas/TenderEligibility' }
    LeadCreateInput:
      type: object
      required: [lead_type, title]
      properties:
        lead_type: { $ref: '#/components/schemas/LeadType' }
        lead_category_id: { type: integer, nullable: true }
        title: { type: string, minLength: 3 }
        description: { type: string, nullable: true }
        client_id: { type: integer, nullable: true }
        client_name: { type: string, nullable: true }
        client_dept: { type: string, nullable: true }
        client_contact: { type: string, nullable: true }
        client_email: { type: string, format: email, nullable: true }
        client_phone: { type: string, nullable: true }
        client_location: { type: string, nullable: true }
        source_id: { type: integer, nullable: true }
        status_id: { type: integer, nullable: true }
        approx_value: { type: number, format: double, nullable: true }
        currency: { type: string, maxLength: 5 }
        priority: { $ref: '#/components/schemas/LeadPriority' }
        assigned_to: { type: integer, nullable: true }
        team_lead_id: { type: integer, nullable: true }
        department_id: { type: integer, nullable: true }
        enquiry_date: { type: string, format: date, nullable: true }
        expected_close_date: { type: string, format: date, nullable: true }
        next_followup_date: { type: string, format: date, nullable: true }
        notes: { type: string, nullable: true }
        force_create:
          type: boolean
          description: Set true to bypass duplicate (phone/email) detection.
    LeadUpdateInput:
      type: object
      properties:
        title: { type: string }
        description: { type: string, nullable: true }
        lead_category_id: { type: integer, nullable: true }
        client_id: { type: integer, nullable: true }
        client_name: { type: string, nullable: true }
        client_dept: { type: string, nullable: true }
        client_contact: { type: string, nullable: true }
        client_email: { type: string, format: email, nullable: true }
        client_phone: { type: string, nullable: true }
        client_location: { type: string, nullable: true }
        source_id: { type: integer, nullable: true }
        status_id: { type: integer, nullable: true }
        priority: { $ref: '#/components/schemas/LeadPriority' }
        assigned_to: { type: integer, nullable: true }
        team_lead_id: { type: integer, nullable: true }
        department_id: { type: integer, nullable: true }
        enquiry_date: { type: string, format: date, nullable: true }
        expected_close_date: { type: string, format: date, nullable: true }
        next_followup_date: { type: string, format: date, nullable: true }
        notes: { type: string, nullable: true }
        probability: { type: integer, minimum: 0, maximum: 100 }
    LeadReviseInput:
      type: object
      required: [revision_type, reason]
      properties:
        new_value: { type: number, format: double, nullable: true }
        new_close_date: { type: string, format: date, nullable: true }
        new_stage: { type: string, nullable: true }
        new_probability: { type: integer, minimum: 0, maximum: 100, nullable: true }
        revision_type: { type: string, enum: [value, date, stage, status, reassign, other] }
        reason: { type: string, minLength: 5 }
    LeadStageInput:
      type: object
      required: [stage]
      properties:
        stage: { $ref: '#/components/schemas/LeadStage' }
        reason: { type: string }
        final_value: { type: number, format: double, nullable: true }
        actual_close_date: { type: string, format: date, nullable: true }
        win_loss_reason: { type: string, nullable: true }
        lost_to_competitor: { type: string, nullable: true }
    LeadRevision:
      type: object
      properties:
        id: { type: integer }
        lead_id: { type: integer }
        old_value: { type: number, format: double, nullable: true }
        new_value: { type: number, format: double, nullable: true }
        old_close_date: { type: string, format: date, nullable: true }
        new_close_date: { type: string, format: date, nullable: true }
        old_stage: { type: string, nullable: true }
        new_stage: { type: string, nullable: true }
        old_probability: { type: integer, nullable: true }
        new_probability: { type: integer, nullable: true }
        revision_type: { type: string, enum: [value, date, stage, status, reassign, other] }
        reason: { type: string }
        revised_by: { type: integer }
        created_at: { type: string, format: date-time }
    LeadPage:
      type: object
      description: Laravel length-aware paginator of leads.
      properties:
        current_page: { type: integer }
        per_page: { type: integer }
        total: { type: integer }
        last_page: { type: integer }
        data:
          type: array
          items: { $ref: '#/components/schemas/Lead' }
    PipelineColumn:
      type: object
      properties:
        leads:
          type: array
          items: { $ref: '#/components/schemas/Lead' }
        count: { type: integer }
        value: { type: number, format: double }

    # ── Lead Category ──────────────────────────────────────────────
    LeadCategory:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        code: { type: string }
        description: { type: string, nullable: true }
        sort_order: { type: integer }
        is_active: { type: boolean }
    LeadCategoryInput:
      type: object
      required: [name]
      properties:
        name: { type: string, maxLength: 100 }
        code: { type: string, maxLength: 20, description: Unique; auto-slugged from name if omitted }
        description: { type: string, nullable: true }
        sort_order: { type: integer }
        is_active: { type: boolean }

    # ── Proposal ───────────────────────────────────────────────────
    Proposal:
      type: object
      properties:
        id: { type: integer }
        lead_id: { type: integer }
        prepared_by: { type: integer }
        approved_by: { type: integer, nullable: true }
        version: { type: integer }
        proposal_no: { type: string, example: PRO-12-v1 }
        quot_no: { type: string, example: Q-2026-001 }
        title: { type: string }
        proposed_value: { type: number, format: double }
        discount_pct: { type: number, format: double }
        gst_type: { $ref: '#/components/schemas/GstType' }
        gst_pct: { type: number, format: double }
        place_of_supply: { type: string, nullable: true }
        payment_terms: { type: string, nullable: true }
        delivery_terms: { type: string, nullable: true }
        status: { $ref: '#/components/schemas/ProposalStatus' }
        submitted_date: { type: string, format: date, nullable: true }
        valid_until: { type: string, format: date, nullable: true }
        scope_summary: { type: string, nullable: true }
        terms_summary: { type: string, nullable: true }
        sub_total: { type: number, format: double }
        discount_amount: { type: number, format: double }
        cgst_amount: { type: number, format: double }
        sgst_amount: { type: number, format: double }
        igst_amount: { type: number, format: double }
        total_gst: { type: number, format: double }
        total_amount: { type: number, format: double }
        items:
          type: array
          items: { $ref: '#/components/schemas/ProposalItem' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ProposalItem:
      type: object
      properties:
        id: { type: integer }
        proposal_id: { type: integer }
        sort_order: { type: integer }
        description: { type: string }
        unit: { type: string, nullable: true }
        quantity: { type: number, format: double }
        rate: { type: number, format: double }
        discount_pct: { type: number, format: double }
        amount: { type: number, format: double }
        gst_pct: { type: number, format: double }
        cgst_amount: { type: number, format: double }
        sgst_amount: { type: number, format: double }
        igst_amount: { type: number, format: double }
        total_amount: { type: number, format: double }
    ProposalInput:
      type: object
      required: [title]
      properties:
        title: { type: string }
        proposed_value: { type: number, format: double, nullable: true }
        discount_pct: { type: number, format: double, minimum: 0, maximum: 100, nullable: true }
        gst_type: { $ref: '#/components/schemas/GstType' }
        gst_pct: { type: number, format: double, minimum: 0, nullable: true }
        place_of_supply: { type: string, maxLength: 100, nullable: true }
        payment_terms: { type: string, maxLength: 255, nullable: true }
        delivery_terms: { type: string, maxLength: 255, nullable: true }
        status: { $ref: '#/components/schemas/ProposalStatus' }
        submitted_date: { type: string, format: date, nullable: true }
        valid_until: { type: string, format: date, nullable: true }
        scope_summary: { type: string, nullable: true }
        terms_summary: { type: string, nullable: true }
        items:
          type: array
          items: { $ref: '#/components/schemas/ProposalItemInput' }
    ProposalItemInput:
      type: object
      required: [description]
      properties:
        description: { type: string }
        unit: { type: string, maxLength: 30, nullable: true }
        quantity: { type: number, format: double, minimum: 0, nullable: true }
        rate: { type: number, format: double, minimum: 0, nullable: true }
        discount_pct: { type: number, format: double, minimum: 0, maximum: 100, nullable: true }
        gst_pct: { type: number, format: double, minimum: 0, nullable: true }

    # ── Contact ────────────────────────────────────────────────────
    Contact:
      type: object
      properties:
        id: { type: integer }
        lead_id: { type: integer }
        name: { type: string }
        designation: { type: string, nullable: true }
        department: { type: string, nullable: true }
        email: { type: string, format: email, nullable: true }
        mobile: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        role: { $ref: '#/components/schemas/ContactRole' }
        is_primary: { type: boolean }
        notes: { type: string, nullable: true }
        party_id: { type: integer, nullable: true }
    ContactInput:
      type: object
      required: [name]
      properties:
        name: { type: string, maxLength: 150 }
        designation: { type: string, maxLength: 100, nullable: true }
        department: { type: string, maxLength: 100, nullable: true }
        email: { type: string, format: email, maxLength: 150, nullable: true }
        mobile: { type: string, maxLength: 30, nullable: true }
        phone: { type: string, maxLength: 30, nullable: true }
        role: { $ref: '#/components/schemas/ContactRole' }
        is_primary: { type: boolean }
        notes: { type: string, maxLength: 255, nullable: true }
        party_id: { type: integer, nullable: true }

    # ── Lead Activity ──────────────────────────────────────────────
    LeadActivity:
      type: object
      properties:
        id: { type: integer }
        lead_id: { type: integer }
        activity_type: { type: string }
        subject: { type: string, nullable: true }
        description: { type: string }
        contact_person: { type: string, nullable: true }
        activity_date: { type: string, format: date }
        activity_time: { type: string, nullable: true }
        outcome: { $ref: '#/components/schemas/ActivityOutcome' }
        next_action_date: { type: string, format: date, nullable: true }
        next_action: { type: string, nullable: true }
        done_by: { type: integer }
        created_at: { type: string, format: date-time }
    LeadActivityInput:
      type: object
      required: [activity_type, description, activity_date]
      properties:
        activity_type: { type: string }
        subject: { type: string, nullable: true }
        description: { type: string, minLength: 3 }
        contact_person: { type: string, nullable: true }
        activity_date: { type: string, format: date }
        activity_time: { type: string, nullable: true }
        outcome: { $ref: '#/components/schemas/ActivityOutcome' }
        next_action_date: { type: string, format: date, nullable: true }
        next_action: { type: string, nullable: true }

    # ── Tender Detail ──────────────────────────────────────────────
    TenderDetail:
      type: object
      properties:
        id: { type: integer }
        lead_id: { type: integer }
        tender_category_id: { type: integer, nullable: true }
        tender_id: { type: string, nullable: true }
        tender_ref: { type: string, nullable: true }
        tender_mode: { type: string, nullable: true }
        portal_name: { type: string, nullable: true }
        portal_url: { type: string, nullable: true }
        issuing_authority: { type: string, nullable: true }
        publication_date: { type: string, format: date, nullable: true }
        pre_bid_date: { type: string, format: date, nullable: true }
        bid_submission_date: { type: string, format: date, nullable: true }
        bid_opening_date: { type: string, format: date, nullable: true }
        technical_opening_date: { type: string, format: date, nullable: true }
        financial_opening_date: { type: string, format: date, nullable: true }
        work_order_date: { type: string, format: date, nullable: true }
        work_start_date: { type: string, format: date, nullable: true }
        completion_date: { type: string, format: date, nullable: true }
        work_duration_months: { type: integer, nullable: true }
        estimated_cost: { type: number, format: double, nullable: true }
        bid_value: { type: number, format: double, nullable: true }
        l1_value: { type: number, format: double, nullable: true }
        is_l1: { type: boolean }
        negotiated_value: { type: number, format: double, nullable: true }
        contract_value: { type: number, format: double, nullable: true }
        bid_status:
          type: string
          description: 'e.g. interested, go_decision, not_go, work_order'
          nullable: true
        is_pre_qualified: { type: boolean, nullable: true }
        is_technical_qualified: { type: boolean, nullable: true }
        disqualification_reason: { type: string, nullable: true }
        special_conditions: { type: string, nullable: true }
        scope_of_work: { type: string, nullable: true }
        go_no_go_by: { type: integer, nullable: true }
        go_no_go_reason: { type: string, nullable: true }
        tender_status_changed_at: { type: string, format: date-time, nullable: true }
    TenderDetailInput:
      type: object
      properties:
        tender_category_id: { type: integer, nullable: true }
        tender_id: { type: string, nullable: true }
        tender_ref: { type: string, nullable: true }
        tender_mode: { type: string, nullable: true }
        portal_name: { type: string, nullable: true }
        portal_url: { type: string, nullable: true }
        issuing_authority: { type: string, nullable: true }
        publication_date: { type: string, format: date, nullable: true }
        pre_bid_date: { type: string, format: date, nullable: true }
        bid_submission_date: { type: string, format: date, nullable: true }
        bid_opening_date: { type: string, format: date, nullable: true }
        technical_opening_date: { type: string, format: date, nullable: true }
        financial_opening_date: { type: string, format: date, nullable: true }
        work_order_date: { type: string, format: date, nullable: true }
        work_start_date: { type: string, format: date, nullable: true }
        completion_date: { type: string, format: date, nullable: true }
        work_duration_months: { type: integer, nullable: true }
        estimated_cost: { type: number, format: double, nullable: true }
        bid_value: { type: number, format: double, nullable: true }
        l1_value: { type: number, format: double, nullable: true }
        is_l1: { type: boolean }
        negotiated_value: { type: number, format: double, nullable: true }
        contract_value: { type: number, format: double, nullable: true }
        bid_status: { type: string, nullable: true }
        is_pre_qualified: { type: boolean, nullable: true }
        is_technical_qualified: { type: boolean, nullable: true }
        disqualification_reason: { type: string, nullable: true }
        special_conditions: { type: string, nullable: true }
        scope_of_work: { type: string, nullable: true }

    # ── EMD / BG ───────────────────────────────────────────────────
    EmdBgRecord:
      type: object
      properties:
        id: { type: integer }
        lead_id: { type: integer }
        emd_bg_type_id: { type: integer, nullable: true }
        amount: { type: number, format: double }
        mode: { $ref: '#/components/schemas/EmdBgMode' }
        instrument_no: { type: string, nullable: true }
        bank_name: { type: string, nullable: true }
        instrument_date: { type: string, format: date, nullable: true }
        validity_date: { type: string, format: date, nullable: true }
        status: { $ref: '#/components/schemas/EmdBgStatus' }
        notes: { type: string, nullable: true }
        created_by: { type: integer }
    EmdBgRecordInput:
      type: object
      required: [amount, mode]
      properties:
        emd_bg_type_id: { type: integer, nullable: true }
        amount: { type: number, format: double, minimum: 0 }
        mode: { $ref: '#/components/schemas/EmdBgMode' }
        instrument_no: { type: string, nullable: true }
        bank_name: { type: string, nullable: true }
        instrument_date: { type: string, format: date, nullable: true }
        validity_date: { type: string, format: date, nullable: true }
        status: { $ref: '#/components/schemas/EmdBgStatus' }
        notes: { type: string, nullable: true }

    # ── Tender Document ────────────────────────────────────────────
    TenderDocument:
      type: object
      properties:
        id: { type: integer }
        lead_id: { type: integer }
        document_type_id: { type: integer, nullable: true }
        document_name: { type: string }
        status: { type: string, nullable: true }
        required_by: { type: string, format: date, nullable: true }
        assigned_to: { type: integer, nullable: true }
        reference_no: { type: string, nullable: true }
        validity_date: { type: string, format: date, nullable: true }
        notes: { type: string, nullable: true }
    TenderDocumentInput:
      type: object
      required: [document_name]
      properties:
        document_type_id: { type: integer, nullable: true }
        document_name: { type: string }
        status: { type: string, nullable: true }
        required_by: { type: string, format: date, nullable: true }
        assigned_to: { type: integer, nullable: true }
        reference_no: { type: string, nullable: true }
        validity_date: { type: string, format: date, nullable: true }
        notes: { type: string, nullable: true }

    # ── Tender Eligibility ─────────────────────────────────────────
    TenderEligibility:
      type: object
      properties:
        id: { type: integer }
        lead_id: { type: integer }
        criteria_id: { type: integer, nullable: true }
        criteria_name: { type: string }
        criteria_description: { type: string, nullable: true }
        match_status: { $ref: '#/components/schemas/MatchStatus' }
        our_capability: { type: string, nullable: true }
        document_ref: { type: string, nullable: true }
        gap_notes: { type: string, nullable: true }
        is_critical: { type: boolean }
        checked_by: { type: integer }
        checked_at: { type: string, format: date-time }
    EligibilityCheckInput:
      type: object
      required: [criteria_name, match_status]
      properties:
        criteria_id: { type: integer, nullable: true }
        criteria_name: { type: string }
        criteria_description: { type: string, nullable: true }
        match_status: { $ref: '#/components/schemas/MatchStatus' }
        our_capability: { type: string, nullable: true }
        document_ref: { type: string, nullable: true }
        gap_notes: { type: string, nullable: true }
        is_critical: { type: boolean }

    # ── Master records ─────────────────────────────────────────────
    LeadSource:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        code: { type: string }
        color: { type: string, nullable: true }
        sort_order: { type: integer }
        is_active: { type: boolean }
    LeadStatus:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        code: { type: string }
        lead_type: { type: string, enum: [open_market, tender, all] }
        color: { type: string, nullable: true }
        stage: { type: string, nullable: true }
        is_won: { type: boolean }
        is_lost: { type: boolean }
        sort_order: { type: integer }
        is_active: { type: boolean }
    TenderCategory:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        code: { type: string }
        tender_type: { type: string, enum: [works, supply, service, consultancy, turnkey, mixed] }
        description: { type: string, nullable: true }
        is_active: { type: boolean }
    DocumentType:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        code: { type: string }
        category: { type: string, enum: [eligibility, financial, technical, statutory, experience, other] }
        is_mandatory: { type: boolean }
        sort_order: { type: integer }
        description: { type: string, nullable: true }
        is_active: { type: boolean }
    EmdBgType:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        code: { type: string }
        type: { type: string, enum: [emd, bg, sd, other] }
        mode: { type: string, nullable: true }
        validity_days: { type: integer, nullable: true }
        notes: { type: string, nullable: true }
        is_active: { type: boolean }
    EligibilityCriteria:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        code: { type: string }
        criteria_type: { type: string }
        description: { type: string, nullable: true }
        sort_order: { type: integer }
        is_active: { type: boolean }
    MastersBundle:
      type: object
      properties:
        sources:
          type: array
          items: { $ref: '#/components/schemas/LeadSource' }
        categories:
          type: array
          items: { $ref: '#/components/schemas/LeadCategory' }
        statuses:
          type: array
          items: { $ref: '#/components/schemas/LeadStatus' }
        tender_cats:
          type: array
          items: { $ref: '#/components/schemas/TenderCategory' }
        doc_types:
          type: array
          items: { $ref: '#/components/schemas/DocumentType' }
        emd_bg_types:
          type: array
          items: { $ref: '#/components/schemas/EmdBgType' }
        eligibility:
          type: array
          items: { $ref: '#/components/schemas/EligibilityCriteria' }

    # ── Import ─────────────────────────────────────────────────────
    ImportResult:
      type: object
      properties:
        imported: { type: integer }
        skipped: { type: integer }
        errors:
          type: array
          items:
            type: object
            properties:
              row: { type: integer }
              reason: { type: string }

    # ── Conversion (S7) ────────────────────────────────────────────
    LeadConvertInput:
      type: object
      properties:
        create_project: { type: boolean }
        project_name: { type: string, nullable: true }
        create_receivable: { type: boolean }
        proposal_id: { type: integer, nullable: true, description: Required when create_receivable is true }
    LeadConvertResult:
      type: object
      properties:
        lead: { $ref: '#/components/schemas/Lead' }
        project: { type: object, additionalProperties: true, nullable: true }
        fin_party: { type: object, additionalProperties: true, nullable: true }
        fin_receivable: { type: object, additionalProperties: true, nullable: true }

    # ── Tender calendar ────────────────────────────────────────────
    TenderCalendar:
      type: object
      properties:
        month: { type: string, example: '2026-08' }
        by_date:
          type: object
          additionalProperties:
            type: array
            items: { $ref: '#/components/schemas/TenderCalendarEvent' }
        tenders:
          type: array
          items: { $ref: '#/components/schemas/TenderCalendarEvent' }
    TenderCalendarEvent:
      type: object
      properties:
        lead_id: { type: integer }
        ref_no: { type: string }
        title: { type: string }
        client_name: { type: string, nullable: true }
        priority: { $ref: '#/components/schemas/LeadPriority' }
        pre_bid_date: { type: string, format: date, nullable: true }
        bid_submission_date: { type: string, format: date, nullable: true }
        bid_opening_date: { type: string, format: date, nullable: true }
        technical_opening_date: { type: string, format: date, nullable: true }

    # ── Dashboard ──────────────────────────────────────────────────
    DashboardData:
      type: object
      properties:
        kpi:
          type: object
          properties:
            total_active: { type: integer }
            active_value: { type: number, format: double }
            won_count: { type: integer }
            won_value: { type: number, format: double }
            lost_count: { type: integer }
            open_tenders: { type: integer }
            win_rate: { type: number, format: double }
            overdue_followups: { type: integer }
            upcoming_bids_30d: { type: integer }
        pipeline:
          type: array
          items:
            type: object
            properties:
              stage: { $ref: '#/components/schemas/LeadStage' }
              count: { type: integer }
              value: { type: number, format: double }
        upcoming_bids:
          type: array
          items: { type: object, additionalProperties: true }
        overdue_followups:
          type: array
          items: { $ref: '#/components/schemas/Lead' }
        trend:
          type: array
          items:
            type: object
            properties:
              month: { type: string, example: Aug 2026 }
              won_value: { type: number, format: double }
              pipeline_count: { type: integer }

    # ── Reports ────────────────────────────────────────────────────
    ReportFunnelRow:
      type: object
      properties:
        stage: { $ref: '#/components/schemas/LeadStage' }
        count: { type: integer }
        total_value: { type: number, format: double }
        avg_days_in_stage: { type: number, format: double }
    SourcePerformanceRow:
      type: object
      properties:
        source_id: { type: integer, nullable: true }
        source_name: { type: string }
        leads_count: { type: integer }
        won_count: { type: integer }
        lost_count: { type: integer }
        win_rate: { type: number, format: double }
        total_won_value: { type: number, format: double }
    SalespersonRow:
      type: object
      properties:
        assigned_to: { type: integer }
        user_name: { type: string }
        leads_count: { type: integer }
        won_count: { type: integer }
        lost_count: { type: integer }
        win_rate: { type: number, format: double }
        overdue_count: { type: integer }
        total_activities: { type: integer }
    TenderWinRate:
      type: object
      properties:
        total_bids: { type: integer }
        won: { type: integer }
        lost: { type: integer }
        withdrawn: { type: integer }
        win_rate: { type: number, format: double }
        avg_bid_value: { type: number, format: double }
        avg_contract_value: { type: number, format: double }
    PipelineAgeingRow:
      type: object
      properties:
        id: { type: integer }
        ref_no: { type: string }
        title: { type: string }
        stage: { $ref: '#/components/schemas/LeadStage' }
        days_in_current_stage: { type: integer, nullable: true }
        last_activity_date: { type: string, format: date, nullable: true }
        days_since_last_activity: { type: integer, nullable: true }

    # ── Audit ──────────────────────────────────────────────────────
    AuditLog:
      type: object
      properties:
        id: { type: integer }
        user_id: { type: integer, nullable: true }
        user:
          type: object
          nullable: true
          properties:
            id: { type: integer }
            name: { type: string }
        module: { type: string, nullable: true }
        method: { type: string }
        entity: { type: string, nullable: true }
        path: { type: string }
        created_at: { type: string, format: date-time }
    AuditLogPage:
      type: object
      description: Laravel length-aware paginator of audit-log entries.
      properties:
        current_page: { type: integer }
        per_page: { type: integer }
        total: { type: integer }
        last_page: { type: integer }
        data:
          type: array
          items: { $ref: '#/components/schemas/AuditLog' }
