openapi: 3.0.3
info:
  title: Lume e-CF
  version: "0.1.0"
  description: |
    Emisión de comprobantes fiscales electrónicos ante la DGII (República Dominicana).

    **Esta referencia no alcanza por sí sola.** Lo difícil de esta integración no son
    las formas de los endpoints, son tres conceptos que conviene leer antes:

    - El **token de DGII** lo obtienes tú, firmando una semilla con el certificado del
      emisor. Lume no custodia certificados y no puede firmarla por ti.
    - **No existe un sandbox permanente.** Un emisor solo puede emitir en `certecf`
      mientras su postulación está activa.
    - El **`Idempotency-Key` es obligatorio**. Sin él, un reintento por timeout
      crearía un segundo comprobante fiscal.

    La guía completa está en `docs/INTEGRACION.md`.
  contact:
    name: Lume — RiserUp SRL
    email: lumeteam@riserup.io

servers:
  - url: https://lume-ecf-staging.up.railway.app
    description: Staging

security:
  - bearerAuth: []

tags:
  - name: Autenticación DGII
  - name: Numeración
  - name: Emisión
  - name: Estado

paths:
  /health:
    get:
      summary: Estado del servicio
      security: []
      tags: [Estado]
      responses:
        "200":
          description: El servicio responde
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  env: { type: string }

  /emisores/{rnc}/token:
    post:
      summary: Entregar el token de DGII del emisor
      description: |
        Transmitir a la DGII exige firmar una semilla con el certificado del emisor y
        cambiarla por un token de ~1 hora. Ese paso lo haces tú; aquí nos entregas el
        resultado. Sin token vigente el comprobante se registra pero no sale.
      tags: [Autenticación DGII]
      parameters:
        - $ref: '#/components/parameters/Rnc'
        - $ref: '#/components/parameters/Actor'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, expira, environment]
              properties:
                token:
                  type: string
                  description: El bearer que devolvió `validarsemilla`
                expira:
                  type: string
                  format: date-time
                  example: "2026-09-01T18:09:38Z"
                environment:
                  $ref: '#/components/schemas/Environment'
      responses:
        "200":
          description: Token guardado
        "400": { $ref: '#/components/responses/Invalido' }
        "401": { $ref: '#/components/responses/NoAutenticado' }
        "403": { $ref: '#/components/responses/NoAutorizado' }

  /reservar-encf:
    post:
      summary: Reservar el siguiente eNCF
      description: |
        Atómico: dos llamadas concurrentes nunca obtienen el mismo número. El eNCF va
        dentro del XML firmado, así que no se puede renumerar después.
      tags: [Numeración]
      parameters:
        - $ref: '#/components/parameters/Actor'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rnc, ecfType, environment]
              properties:
                rnc: { type: string, example: "131123456" }
                ecfType:
                  type: string
                  description: Tipo de e-CF
                  enum: ["31","32","33","34","41","43","44","45","46","47"]
                environment: { $ref: '#/components/schemas/Environment' }
      responses:
        "200":
          description: eNCF reservado
          content:
            application/json:
              schema:
                type: object
                properties:
                  encf: { type: string, example: "E310000001501" }
                  vencimiento: { type: string, nullable: true, example: "31-12-2026" }
        "400": { $ref: '#/components/responses/Invalido' }
        "401": { $ref: '#/components/responses/NoAutenticado' }
        "403": { $ref: '#/components/responses/NoAutorizado' }

  /submit:
    post:
      summary: Emitir un comprobante ya firmado
      description: |
        Responde `202`, no `200`: aceptamos el comprobante y la DGII decide después.
        Tu cajero no espera.

        Imprime de inmediato — el `codigo_seguridad` son los primeros 6 caracteres del
        `SignatureValue` de tu propio XML, así que no necesitas nuestra respuesta para
        armar el Timbre y el QR.
      tags: [Emisión]
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema: { type: string }
          description: |
            Tu identificador de venta. Reintentar con la misma clave devuelve el mismo
            comprobante con `idempotente: true` y nunca crea un segundo.
        - $ref: '#/components/parameters/Actor'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [xml, kind, environment]
              properties:
                xml:
                  type: string
                  description: El e-CF firmado. Debe contener SignatureValue
                kind:
                  type: string
                  enum: [ecf, rfce]
                  description: '`rfce` solo para el resumen de consumo final'
                environment: { $ref: '#/components/schemas/Environment' }
                referencia:
                  type: string
                  description: Tu propia referencia, para buscarlo después
                encf:
                  type: string
                  description: Opcional; si no viene se toma del XML
      responses:
        "202":
          description: Aceptado y transmitido a la DGII
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Documento' }
        "200":
          description: Reintento con la misma clave. Es el mismo comprobante, no uno nuevo
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Documento'
                  - type: object
                    properties:
                      idempotente: { type: boolean, example: true }
        "400": { $ref: '#/components/responses/Invalido' }
        "401": { $ref: '#/components/responses/NoAutenticado' }
        "403": { $ref: '#/components/responses/NoAutorizado' }
        "409":
          description: |
            No hay token de DGII vigente. **El comprobante quedó registrado** y sale
            solo cuando entregues uno.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorAccionable' }
        "422":
          description: El XML no está firmado, o la DGII lo rechazó al recibirlo
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorAccionable' }

  /documentos/{id}:
    get:
      summary: Estado de un comprobante
      description: |
        Nosotros consultamos el veredicto y reconciliamos. No tienes que hacer polling
        contra la DGII.
      tags: [Estado]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: El comprobante y sus transiciones
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Documento'
                  - type: object
                    properties:
                      rnc: { type: string }
                      referencia: { type: string, nullable: true }
                      dgii:
                        type: object
                        properties:
                          respuesta_cruda:
                            type: string
                            nullable: true
                            description: La respuesta de la DGII **sin traducir**
                          recibida_at: { type: string, format: date-time, nullable: true }
                      transiciones:
                        type: array
                        items:
                          type: object
                          properties:
                            estado: { $ref: '#/components/schemas/Estado' }
                            creado_at: { type: string, format: date-time }
        "401": { $ref: '#/components/responses/NoAutenticado' }
        "403": { $ref: '#/components/responses/NoAutorizado' }
        "404": { description: No existe }

  /consulta:
    post:
      summary: Consultar un trackId directamente
      description: Normalmente no hace falta — el módulo reconcilia solo.
      tags: [Estado]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [trackId, rnc, environment]
              properties:
                trackId: { type: string }
                rnc: { type: string }
                environment: { $ref: '#/components/schemas/Environment' }
      responses:
        "200":
          description: La respuesta de la DGII, tal cual
        "400": { $ref: '#/components/responses/Invalido' }
        "401": { $ref: '#/components/responses/NoAutenticado' }
        "403": { $ref: '#/components/responses/NoAutorizado' }
        "409":
          description: No hay token de DGII vigente
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorAccionable' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Tu credencial identifica a **tu sistema**, y está autorizada sobre una lista
        explícita de RNC emisores. El RNC viaja siempre explícito en cada petición.

  parameters:
    Rnc:
      name: rnc
      in: path
      required: true
      schema: { type: string }
      example: "131123456"
    Actor:
      name: X-Lume-Actor
      in: header
      required: false
      schema: { type: string }
      description: |
        Quién dentro de tu sistema disparó la operación. Se guarda para auditoría.
        **No otorga permisos.**

  schemas:
    Environment:
      type: string
      enum: [certecf, ecf]
      description: |
        Obligatorio en cada petición, sin valor por defecto. `testecf` está rechazado.
        Un emisor solo puede usar `certecf` mientras su postulación esté activa.

    Estado:
      type: string
      enum: [recibido, transmitido, aceptado, rechazado]

    Documento:
      type: object
      properties:
        id: { type: string, format: uuid }
        encf: { type: string, example: "E310000001501" }
        estado: { $ref: '#/components/schemas/Estado' }
        track_id: { type: string, nullable: true }
        codigo_seguridad:
          type: string
          nullable: true
          description: Primeros 6 caracteres del SignatureValue
        qr: { type: string, nullable: true }

    ErrorAccionable:
      type: object
      properties:
        error: { type: string }
        como_resolver:
          type: string
          description: Qué hacer, cuando hay algo concreto que hacer
        por_que: { type: string }

  responses:
    Invalido:
      description: Falta un campo obligatorio o tiene un valor inválido
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorAccionable' }
    NoAutenticado:
      description: Falta la credencial o no es válida
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorAccionable' }
    NoAutorizado:
      description: Tu credencial no está autorizada sobre ese emisor
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorAccionable' }
