openapi: 3.0.3
info:
  title: FE Public API
  version: 1.0.0
  description: API publica para integraciones de facturacion electronica.

servers:
  - url: https://tu-dominio.com

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
  schemas:
    ApiError:
      type: object
      properties:
        error:
          type: string
        code:
          type: string
        fields:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
    Pagination:
      type: object
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
        pages:
          type: integer
    IdempotencyKey:
      type: object
      properties:
        id:
          type: string
        keyMasked:
          type: string
        method:
          type: string
        route:
          type: string
        statusCode:
          type: integer
          nullable: true
        status:
          type: string
          enum: [COMPLETED, IN_PROGRESS]
        completedAt:
          type: string
          format: date-time
          nullable: true
        expiresAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    IdempotencyCleanupResult:
      type: object
      properties:
        deleted:
          type: integer
        expired:
          type: integer
        staleCompleted:
          type: integer
        staleInProgress:
          type: integer
    InvoiceItemInput:
      type: object
      required: [nombre, cantidad, precioUnitario, indicadorFacturacion, indicadorBienServicio]
      properties:
        nombre:
          type: string
          maxLength: 80
        descripcion:
          type: string
          maxLength: 1000
        cantidad:
          type: number
          minimum: 0
          exclusiveMinimum: true
        precioUnitario:
          type: number
          minimum: 0
        descuento:
          type: number
          minimum: 0
        indicadorFacturacion:
          type: integer
          minimum: 0
          maximum: 4
        indicadorBienServicio:
          type: integer
          enum: [1, 2]
        unidadMedida:
          type: integer
    CreateInvoiceInput:
      type: object
      required: [tipoEcf, tipoPago, items]
      properties:
        tipoEcf:
          type: integer
          enum: [31, 32, 33, 34, 41, 43, 44, 45, 46, 47]
        tipoPago:
          type: integer
          enum: [1, 2, 3]
        tipoIngresos:
          type: integer
          minimum: 1
          maximum: 6
        fechaVencimientoSecuencia:
          type: string
          description: Fecha en formato DGII DD-MM-YYYY.
          example: 31-12-2026
        numeroInterno:
          type: string
          maxLength: 20
        comprador:
          type: object
          properties:
            rnc:
              type: string
              pattern: '^\\d{9}(\\d{2})?$'
            nombre:
              type: string
              maxLength: 150
            email:
              type: string
              format: email
            direccion:
              type: string
              maxLength: 100
        items:
          type: array
          minItems: 1
          maxItems: 1000
          items:
            $ref: '#/components/schemas/InvoiceItemInput'
    InvoiceItem:
      type: object
      properties:
        id:
          type: string
        numeroLinea:
          type: integer
        nombreItem:
          type: string
        descripcion:
          type: string
          nullable: true
        cantidad:
          type: number
        precioUnitario:
          type: number
        montoItem:
          type: number
        tasaItbis:
          type: number
        montoItbis:
          type: number
    Invoice:
      type: object
      properties:
        id:
          type: string
        eNcf:
          type: string
          nullable: true
        tipoEcf:
          type: string
          enum: [ECF_31, ECF_32, ECF_33, ECF_34, ECF_41, ECF_43, ECF_44, ECF_45, ECF_46, ECF_47]
        estado:
          type: string
          enum: [BORRADOR, PENDIENTE, APROBADA, RECHAZADA, ANULADA, CONTINGENCIA]
        numeroInterno:
          type: string
          nullable: true
        fechaEmision:
          type: string
          format: date-time
        montoGravadoTotal:
          type: number
        montoExento:
          type: number
        totalItbis:
          type: number
        montoTotal:
          type: number
        customer:
          $ref: '#/components/schemas/InvoiceCustomer'
        items:
          type: array
          items:
            $ref: '#/components/schemas/InvoiceItem'
    InvoiceCustomer:
      type: object
      nullable: true
      properties:
        id:
          type: string
        razonSocial:
          type: string
        rnc:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
        direccion:
          type: string
          nullable: true
    InvoiceStatus:
      type: object
      properties:
        id:
          type: string
        estado:
          type: string
        eNcf:
          type: string
          nullable: true
        dgiiRespuestaCodigo:
          type: string
          nullable: true
        dgiiRespuestaMensaje:
          type: string
          nullable: true
        dgiiTimestamp:
          type: string
          format: date-time
          nullable: true
        intentosEnvio:
          type: integer
        ultimoIntentoEnvio:
          type: string
          format: date-time
          nullable: true
    EmissionResult:
      type: object
      properties:
        eNcf:
          type: string
        trackId:
          type: string
        mensaje:
          type: string
    CancelInvoiceInput:
      type: object
      required: [motivo]
      properties:
        motivo:
          type: string
          minLength: 5
          maxLength: 250
    CanceledInvoice:
      type: object
      properties:
        id:
          type: string
        estado:
          type: string
        eNcf:
          type: string
          nullable: true
        anuladaEn:
          type: string
          format: date-time
          nullable: true
        motivoAnulacion:
          type: string
          nullable: true
    CustomerInput:
      type: object
      required: [razonSocial]
      properties:
        razonSocial:
          type: string
          minLength: 2
          maxLength: 150
        rnc:
          type: string
          pattern: '^\\d{9}(\\d{2})?$'
        email:
          type: string
          format: email
        telefono:
          type: string
          maxLength: 20
        direccion:
          type: string
          maxLength: 100
        municipioCodigo:
          type: string
          pattern: '^\\d{6}$'
        provinciaCodigo:
          type: string
          pattern: '^\\d{6}$'
    Customer:
      type: object
      properties:
        id:
          type: string
        razonSocial:
          type: string
        rnc:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
        telefono:
          type: string
          nullable: true
        direccion:
          type: string
          nullable: true
        municipio:
          type: string
          nullable: true
        provincia:
          type: string
          nullable: true
        municipioCodigo:
          type: string
          nullable: true
        provinciaCodigo:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CatalogItem:
      type: object
      properties:
        code:
          oneOf:
            - type: string
            - type: integer
        name:
          type: string
        provinceCode:
          type: string
    WebhookEndpoint:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        url:
          type: string
          format: uri
        active:
          type: boolean
        events:
          type: array
          items:
            type: string
            enum: [invoice.approved, invoice.rejected, invoice.canceled]
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CreateWebhookEndpointInput:
      type: object
      required: [name, url]
      properties:
        name:
          type: string
          minLength: 3
          maxLength: 100
        url:
          type: string
          format: uri
          maxLength: 500
        events:
          type: array
          items:
            type: string
            enum: [invoice.approved, invoice.rejected, invoice.canceled]
    WebhookEndpointWithSecret:
      allOf:
        - $ref: '#/components/schemas/WebhookEndpoint'
        - type: object
          properties:
            signingSecret:
              type: string
            supportedEvents:
              type: array
              items:
                type: string
    WebhookDelivery:
      type: object
      properties:
        id:
          type: string
        event:
          type: string
          enum: [invoice.approved, invoice.rejected, invoice.canceled]
        eventType:
          type: string
        eventId:
          type: string
        status:
          type: string
          enum: [PENDING, PROCESSING, SUCCESS, FAILED]
        attempts:
          type: integer
        responseStatus:
          type: integer
          nullable: true
        lastError:
          type: string
          nullable: true
        deliveredAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
    WebhookProcessResult:
      type: object
      properties:
        processed:
          type: integer
        success:
          type: integer
        failed:
          type: integer
        queued:
          type: integer
    DataEnvelope:
      type: object
      properties:
        data:
          nullable: true
  parameters:
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: false
      schema:
        type: string
      description: Clave de idempotencia para operaciones POST/PATCH/DELETE.

security:
  - bearerAuth: []

paths:
  /api/v1/invoices:
    get:
      summary: Listar facturas
      parameters:
        - in: query
          name: page
          schema:
            type: integer
        - in: query
          name: limit
          schema:
            type: integer
        - in: query
          name: estado
          schema:
            type: string
        - in: query
          name: tipoEcf
          schema:
            type: string
        - in: query
          name: desde
          schema:
            type: string
            format: date
        - in: query
          name: hasta
          schema:
            type: string
            format: date
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Invoice'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: No autorizado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'

    post:
      summary: Crear factura en borrador
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateInvoiceInput'
      responses:
        '201':
          description: Creada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Invoice'
        '422':
          description: Error de validacion
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'

  /api/v1/invoices/{id}:
    get:
      summary: Obtener detalle de factura
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Invoice'
        '404':
          description: No encontrada

  /api/v1/invoices/{id}/emit:
    post:
      summary: Emitir factura a DGII
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Emitida o en proceso
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/EmissionResult'
        '422':
          description: Rechazada por regla de negocio

  /api/v1/invoices/{id}/status:
    get:
      summary: Consultar estado de una factura
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/InvoiceStatus'
        '404':
          description: No encontrada

  /api/v1/invoices/{id}/retry-emit:
    post:
      summary: Reintentar emisión de factura
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Reintentada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/EmissionResult'
        '422':
          description: Error de emisión

  /api/v1/invoices/{id}/cancel:
    post:
      summary: Anular factura
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CancelInvoiceInput'
      responses:
        '200':
          description: Anulada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CanceledInvoice'
        '409':
          description: Ya anulada
        '422':
          description: No anulable en el estado actual
        '502':
          description: Error al reportar anulación en DGII

  /api/v1/invoices/{id}/pdf:
    get:
      summary: Descargar/visualizar PDF de factura
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
        - in: query
          name: dl
          schema:
            type: string
            enum: ['1']
      responses:
        '200':
          description: PDF
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '404':
          description: No encontrada

  /api/v1/invoices/{id}/xml:
    get:
      summary: Obtener XML firmado o generado
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
        - in: query
          name: format
          schema:
            type: string
            enum: [signed, generated]
        - in: query
          name: dl
          schema:
            type: string
            enum: ['1']
      responses:
        '200':
          description: XML
          content:
            application/xml:
              schema:
                type: string
        '404':
          description: No encontrado

  /api/v1/customers:
    get:
      summary: Listar clientes
      parameters:
        - in: query
          name: page
          schema:
            type: integer
        - in: query
          name: limit
          schema:
            type: integer
        - in: query
          name: q
          schema:
            type: string
        - in: query
          name: rnc
          schema:
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Customer'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: No autorizado

    post:
      summary: Crear cliente
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerInput'
      responses:
        '201':
          description: Creado
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Customer'
        '409':
          description: RNC duplicado

  /api/v1/customers/{id}:
    get:
      summary: Obtener cliente
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Customer'
        '404':
          description: No encontrado

    patch:
      summary: Actualizar cliente
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerInput'
      responses:
        '200':
          description: Actualizado
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Customer'
        '404':
          description: No encontrado

  /api/v1/catalogs:
    get:
      summary: Consultar catálogos DGII
      description: |
        Si no envías `resource`, devuelve todos los catálogos.
        Recursos soportados: `unit-measures`, `currencies`, `provinces`, `municipalities`.
      parameters:
        - in: query
          name: resource
          schema:
            type: string
        - in: query
          name: provinceCode
          schema:
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    oneOf:
                      - type: array
                        items:
                          $ref: '#/components/schemas/CatalogItem'
                      - type: object
        '400':
          description: Recurso inválido

  /api/v1/idempotency/keys:
    get:
      summary: Listar llaves de idempotencia
      parameters:
        - in: query
          name: page
          schema:
            type: integer
        - in: query
          name: limit
          schema:
            type: integer
        - in: query
          name: method
          schema:
            type: string
        - in: query
          name: route
          schema:
            type: string
        - in: query
          name: status
          schema:
            type: string
            enum: [COMPLETED, IN_PROGRESS]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/IdempotencyKey'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          description: Filtro inválido

  /api/v1/idempotency/cleanup:
    post:
      summary: Limpiar llaves de idempotencia expiradas/stale
      responses:
        '200':
          description: Limpieza ejecutada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/IdempotencyCleanupResult'
        '500':
          description: Error inesperado

  /api/v1/webhooks/endpoints:
    get:
      summary: Listar endpoints webhook
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookEndpoint'
    post:
      summary: Crear endpoint webhook
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookEndpointInput'
      responses:
        '201':
          description: Creado
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/WebhookEndpointWithSecret'

  /api/v1/webhooks/endpoints/{id}:
    delete:
      summary: Desactivar endpoint webhook
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Desactivado
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      success:
                        type: boolean
        '404':
          description: No encontrado

  /api/v1/webhooks/deliveries:
    get:
      summary: Listar entregas de webhook
      parameters:
        - in: query
          name: page
          schema:
            type: integer
        - in: query
          name: limit
          schema:
            type: integer
        - in: query
          name: endpointId
          schema:
            type: string
        - in: query
          name: event
          schema:
            type: string
            enum: [invoice.approved, invoice.rejected, invoice.canceled]
        - in: query
          name: status
          schema:
            type: string
            enum: [PENDING, PROCESSING, SUCCESS, FAILED]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookDelivery'
                  pagination:
                    $ref: '#/components/schemas/Pagination'

  /api/v1/webhooks/deliveries/{id}:
    get:
      summary: Obtener detalle de entrega webhook
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                      - $ref: '#/components/schemas/WebhookDelivery'
                      - type: object
                        properties:
                          payload:
                            type: object
                          responseBody:
                            type: string
                            nullable: true
        '404':
          description: No encontrado

  /api/v1/webhooks/deliveries/{id}/retry:
    post:
      summary: Reintentar entrega de webhook
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Reintentado (ejecuta intento inmediato)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/WebhookDelivery'
        '404':
          description: No encontrado
        '409':
          description: Ya exitoso

  /api/v1/webhooks/process:
    post:
      summary: Procesar cola asíncrona de webhooks
      description: Ejecuta intentos para entregas pendientes/fallidas listas para reintento.
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Cola procesada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/WebhookProcessResult'
        '400':
          description: Límite inválido
