openapi: 3.0.3
info:
  title: ERP Construccion API
  description: |
    API REST para sistema de administracion de obra e INFONAVIT.

    ## Autenticacion

    La API utiliza JWT Bearer tokens. Incluir el header:
    ```
    Authorization: Bearer <access_token>
    ```

    ## Multi-tenancy

    Todas las operaciones estan aisladas por tenant_id.
    El tenant se determina automaticamente desde el JWT.

    ## Paginacion

    Los endpoints de listado soportan paginacion:
    - `page`: Numero de pagina (default: 1)
    - `limit`: Items por pagina (default: 20, max: 100)

  version: 1.0.0
  contact:
    name: ERP Construccion Team
    email: dev@construccion.local

servers:
  - url: http://localhost:3000/api/v1
    description: Development
  - url: https://api.construccion.example.com/api/v1
    description: Production

tags:
  - name: Auth
    description: Autenticacion y autorizacion
  - name: Conceptos
    description: Catalogo de conceptos de obra
  - name: Presupuestos
    description: Presupuestos de obra
  - name: Avances
    description: Control de avances fisicos
  - name: Bitacora
    description: Bitacora de obra
  - name: Estimaciones
    description: Estimaciones periodicas

paths:
  /auth/login:
    post:
      tags: [Auth]
      summary: Login de usuario
      operationId: login
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Login exitoso
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/register:
    post:
      tags: [Auth]
      summary: Registro de usuario
      operationId: register
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
      responses:
        '201':
          description: Usuario registrado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        '400':
          $ref: '#/components/responses/BadRequest'

  /auth/refresh:
    post:
      tags: [Auth]
      summary: Renovar access token
      operationId: refreshToken
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefreshTokenRequest'
      responses:
        '200':
          description: Tokens renovados
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/logout:
    post:
      tags: [Auth]
      summary: Logout (revocar refresh token)
      operationId: logout
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefreshTokenRequest'
      responses:
        '204':
          description: Logout exitoso

  /conceptos:
    get:
      tags: [Conceptos]
      summary: Listar conceptos
      operationId: listConceptos
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/LimitParam'
      responses:
        '200':
          description: Lista de conceptos
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConceptoListResponse'
    post:
      tags: [Conceptos]
      summary: Crear concepto
      operationId: createConcepto
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateConceptoRequest'
      responses:
        '201':
          description: Concepto creado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Concepto'

  /conceptos/tree:
    get:
      tags: [Conceptos]
      summary: Obtener arbol de conceptos
      operationId: getConceptoTree
      security:
        - bearerAuth: []
      parameters:
        - name: rootId
          in: query
          schema:
            type: string
            format: uuid
          description: ID del concepto raiz (opcional)
      responses:
        '200':
          description: Arbol de conceptos
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ConceptoTree'

  /conceptos/{id}:
    get:
      tags: [Conceptos]
      summary: Obtener concepto por ID
      operationId: getConcepto
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Concepto encontrado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Concepto'
        '404':
          $ref: '#/components/responses/NotFound'

  /presupuestos:
    get:
      tags: [Presupuestos]
      summary: Listar presupuestos
      operationId: listPresupuestos
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/LimitParam'
        - name: fraccionamientoId
          in: query
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Lista de presupuestos
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PresupuestoListResponse'
    post:
      tags: [Presupuestos]
      summary: Crear presupuesto
      operationId: createPresupuesto
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePresupuestoRequest'
      responses:
        '201':
          description: Presupuesto creado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Presupuesto'

  /presupuestos/{id}:
    get:
      tags: [Presupuestos]
      summary: Obtener presupuesto con partidas
      operationId: getPresupuesto
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Presupuesto con partidas
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PresupuestoDetalle'

  /presupuestos/{id}/partidas:
    post:
      tags: [Presupuestos]
      summary: Agregar partida al presupuesto
      operationId: addPartida
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddPartidaRequest'
      responses:
        '201':
          description: Partida agregada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PresupuestoPartida'

  /presupuestos/{id}/approve:
    post:
      tags: [Presupuestos]
      summary: Aprobar presupuesto
      operationId: approvePresupuesto
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Presupuesto aprobado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Presupuesto'

  /avances:
    get:
      tags: [Avances]
      summary: Listar avances
      operationId: listAvances
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/LimitParam'
        - name: loteId
          in: query
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/AdvanceStatus'
      responses:
        '200':
          description: Lista de avances
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AvanceListResponse'
    post:
      tags: [Avances]
      summary: Crear avance
      operationId: createAvance
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAvanceRequest'
      responses:
        '201':
          description: Avance creado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AvanceObra'

  /avances/{id}/fotos:
    post:
      tags: [Avances]
      summary: Agregar foto al avance
      operationId: addFotoAvance
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddFotoRequest'
      responses:
        '201':
          description: Foto agregada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FotoAvance'

  /avances/{id}/approve:
    post:
      tags: [Avances]
      summary: Aprobar avance
      operationId: approveAvance
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Avance aprobado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AvanceObra'

  /estimaciones:
    get:
      tags: [Estimaciones]
      summary: Listar estimaciones
      operationId: listEstimaciones
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/LimitParam'
        - name: contratoId
          in: query
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/EstimateStatus'
      responses:
        '200':
          description: Lista de estimaciones
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EstimacionListResponse'
    post:
      tags: [Estimaciones]
      summary: Crear estimacion
      operationId: createEstimacion
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateEstimacionRequest'
      responses:
        '201':
          description: Estimacion creada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Estimacion'

  /estimaciones/{id}/submit:
    post:
      tags: [Estimaciones]
      summary: Enviar estimacion para revision
      operationId: submitEstimacion
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Estimacion enviada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Estimacion'

  /estimaciones/{id}/approve:
    post:
      tags: [Estimaciones]
      summary: Aprobar estimacion
      operationId: approveEstimacion
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Estimacion aprobada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Estimacion'

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

  parameters:
    IdParam:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    PageParam:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    LimitParam:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20

  responses:
    BadRequest:
      description: Solicitud invalida
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: No autorizado
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Recurso no encontrado
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

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

    PaginationMeta:
      type: object
      properties:
        total:
          type: integer
        page:
          type: integer
        limit:
          type: integer
        totalPages:
          type: integer

    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 8
        tenantId:
          type: string
          format: uuid

    RegisterRequest:
      type: object
      required: [email, password, firstName, lastName, tenantId]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 8
        firstName:
          type: string
        lastName:
          type: string
        tenantId:
          type: string
          format: uuid

    RefreshTokenRequest:
      type: object
      required: [refreshToken]
      properties:
        refreshToken:
          type: string

    AuthResponse:
      type: object
      properties:
        accessToken:
          type: string
        refreshToken:
          type: string
        expiresIn:
          type: integer
        user:
          type: object
          properties:
            id:
              type: string
              format: uuid
            email:
              type: string
            firstName:
              type: string
            lastName:
              type: string
            roles:
              type: array
              items:
                type: string
        tenant:
          type: object
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string

    Concepto:
      type: object
      properties:
        id:
          type: string
          format: uuid
        code:
          type: string
        name:
          type: string
        description:
          type: string
        parentId:
          type: string
          format: uuid
        level:
          type: integer
        path:
          type: string
        unitPrice:
          type: number
        isComposite:
          type: boolean
        createdAt:
          type: string
          format: date-time

    ConceptoTree:
      allOf:
        - $ref: '#/components/schemas/Concepto'
        - type: object
          properties:
            children:
              type: array
              items:
                $ref: '#/components/schemas/ConceptoTree'

    CreateConceptoRequest:
      type: object
      required: [code, name]
      properties:
        code:
          type: string
          maxLength: 50
        name:
          type: string
          maxLength: 255
        description:
          type: string
        parentId:
          type: string
          format: uuid
        unitPrice:
          type: number
        isComposite:
          type: boolean

    ConceptoListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Concepto'
        meta:
          $ref: '#/components/schemas/PaginationMeta'

    Presupuesto:
      type: object
      properties:
        id:
          type: string
          format: uuid
        code:
          type: string
        name:
          type: string
        version:
          type: integer
        isActive:
          type: boolean
        totalAmount:
          type: number
        approvedAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time

    PresupuestoPartida:
      type: object
      properties:
        id:
          type: string
          format: uuid
        conceptoId:
          type: string
          format: uuid
        quantity:
          type: number
        unitPrice:
          type: number
        totalAmount:
          type: number
        sequence:
          type: integer

    PresupuestoDetalle:
      allOf:
        - $ref: '#/components/schemas/Presupuesto'
        - type: object
          properties:
            partidas:
              type: array
              items:
                $ref: '#/components/schemas/PresupuestoPartida'

    CreatePresupuestoRequest:
      type: object
      required: [code, name]
      properties:
        code:
          type: string
        name:
          type: string
        description:
          type: string
        fraccionamientoId:
          type: string
          format: uuid
        prototipoId:
          type: string
          format: uuid

    AddPartidaRequest:
      type: object
      required: [conceptoId, quantity, unitPrice]
      properties:
        conceptoId:
          type: string
          format: uuid
        quantity:
          type: number
        unitPrice:
          type: number
        sequence:
          type: integer

    PresupuestoListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Presupuesto'
        meta:
          $ref: '#/components/schemas/PaginationMeta'

    AdvanceStatus:
      type: string
      enum: [pending, captured, reviewed, approved, rejected]

    AvanceObra:
      type: object
      properties:
        id:
          type: string
          format: uuid
        loteId:
          type: string
          format: uuid
        conceptoId:
          type: string
          format: uuid
        captureDate:
          type: string
          format: date
        quantityExecuted:
          type: number
        percentageExecuted:
          type: number
        status:
          $ref: '#/components/schemas/AdvanceStatus'
        notes:
          type: string
        createdAt:
          type: string
          format: date-time

    FotoAvance:
      type: object
      properties:
        id:
          type: string
          format: uuid
        fileUrl:
          type: string
        fileName:
          type: string
        description:
          type: string
        capturedAt:
          type: string
          format: date-time

    CreateAvanceRequest:
      type: object
      required: [conceptoId, captureDate, quantityExecuted]
      properties:
        loteId:
          type: string
          format: uuid
        departamentoId:
          type: string
          format: uuid
        conceptoId:
          type: string
          format: uuid
        captureDate:
          type: string
          format: date
        quantityExecuted:
          type: number
        percentageExecuted:
          type: number
        notes:
          type: string

    AddFotoRequest:
      type: object
      required: [fileUrl]
      properties:
        fileUrl:
          type: string
        fileName:
          type: string
        description:
          type: string
        location:
          type: object
          properties:
            lat:
              type: number
            lng:
              type: number

    AvanceListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/AvanceObra'
        meta:
          $ref: '#/components/schemas/PaginationMeta'

    EstimateStatus:
      type: string
      enum: [draft, submitted, reviewed, approved, invoiced, paid, rejected, cancelled]

    Estimacion:
      type: object
      properties:
        id:
          type: string
          format: uuid
        estimateNumber:
          type: string
        contratoId:
          type: string
          format: uuid
        periodStart:
          type: string
          format: date
        periodEnd:
          type: string
          format: date
        sequenceNumber:
          type: integer
        status:
          $ref: '#/components/schemas/EstimateStatus'
        subtotal:
          type: number
        advanceAmount:
          type: number
        retentionAmount:
          type: number
        taxAmount:
          type: number
        totalAmount:
          type: number
        approvedAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time

    CreateEstimacionRequest:
      type: object
      required: [contratoId, fraccionamientoId, periodStart, periodEnd]
      properties:
        contratoId:
          type: string
          format: uuid
        fraccionamientoId:
          type: string
          format: uuid
        periodStart:
          type: string
          format: date
        periodEnd:
          type: string
          format: date
        notes:
          type: string

    EstimacionListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Estimacion'
        meta:
          $ref: '#/components/schemas/PaginationMeta'
