{
  "openapi": "3.0.3",
  "info": {
    "title": "API de Partners — Firma Digital",
    "version": "1.0.0",
    "description": "Canje de códigos de compra, obtención del certificado emitido y consulta de estado, para partners integrados con Firma Digital. Reemplaza las versiones v2/v3 anteriores: un solo flujo de canje cuyo comportamiento (campos requeridos, si valida contra Clave Única o solo contra Registro Civil) se ajusta automáticamente según la configuración del partner.\n\nEsta es la v1: la versión vive en la URL (`/api/partners/v1`). Una eventual v2 conviviría como una sección aparte, sin romper esta."
  },
  "servers": [
    { "url": "https://dev-api.tsp.cl/firma_digital/dev/api/partners/v1", "description": "Desarrollo" },
    { "url": "https://api.firma.digital/api/partners/v1", "description": "Producción" }
  ],
  "components": {
    "securitySchemes": {
      "PartnerApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-gw",
        "description": "API Key del partner, entregada al configurar la integración."
      }
    },
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": false },
          "error": { "type": "string", "example": "VALIDATION_ERROR" },
          "message": { "type": "string" }
        }
      },
      "Titular": {
        "type": "object",
        "properties": {
          "rut": { "type": "string", "example": "18925910-7" },
          "nombreCompleto": { "type": "string", "example": "FABIAN JESUS AGUILAR ALISTE" },
          "email": { "type": "string", "format": "email" }
        }
      }
    }
  },
  "security": [{ "PartnerApiKey": [] }],
  "paths": {
    "/certificate_request": {
      "post": {
        "summary": "Canjear un código de compra (validar identidad)",
        "description": "Valida la identidad del titular de un código de compra ya emitido (vía `POST /pcode` del Worker `partner-pcode`). No entrega el certificado en esta llamada — para eso está `certificate_claim` — salvo que el partner esté configurado en modo B2B, en cuyo caso el certificado se genera y se entrega automáticamente (webhook y/o email al partner) en esta misma llamada, sin que el usuario final tenga que reclamarlo.\n\nLos campos que se exigen dependen de `partner_config.require_cedula_serie`, no de esta llamada: un partner con flujo reducido (`require_cedula_serie=1`) no debe enviar `names`/`fLastname`/`phone` (los nombres se obtienen de Registro Civil) pero sí `serialNumber`; un partner con flujo completo debe enviar `names`/`fLastname`/`phone` y valida además contra Clave Única.",
        "operationId": "certificateRequest",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["pCode", "referenceId", "dni", "email", "pin"],
                "properties": {
                  "pCode": { "type": "string", "description": "Código de compra generado previamente para este partner." },
                  "referenceId": { "type": "string", "description": "Identificador propio del partner para esta transacción." },
                  "dni": { "type": "string", "example": "18925910-7", "description": "RUT del titular, con dígito verificador." },
                  "email": { "type": "string", "format": "email" },
                  "pin": { "type": "string", "pattern": "^\\d{4}$", "description": "4 dígitos — funciona como contraseña del PFX resultante." },
                  "serialNumber": { "type": "string", "description": "Número de serie de la cédula. Obligatorio si el partner tiene flujo reducido (`require_cedula_serie=1`)." },
                  "names": { "type": "string", "description": "Nombres del titular. Obligatorio solo en flujo completo." },
                  "fLastname": { "type": "string", "description": "Apellido paterno. Obligatorio solo en flujo completo." },
                  "mLastname": { "type": "string" },
                  "phone": { "type": "string", "example": "+56911111111", "description": "Obligatorio solo en flujo completo." },
                  "claveUnica": { "type": "string", "description": "Token de Clave Única, si el flujo del partner la requiere." },
                  "dniFront": { "type": "string" },
                  "dniBack": { "type": "string" },
                  "partnerCopy": { "type": "string", "format": "email", "description": "Email adicional al que enviar copia del certificado (flujo completo)." },
                  "fechaVencimiento": {
                    "type": "string",
                    "description": "Fecha de vencimiento de la cédula informada por el usuario, formato `YYYY-MM-DD` o `DD-MM-YYYY`/`DD/MM/YYYY` (día primero). Se compara contra la fecha real de Registro Civil si el partner tiene `validate_vencimiento=1`.",
                    "example": "21/09/2034"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Identidad validada. `status` indica si falta reclamar el certificado (`VALIDATED`, flujo B2C) o si ya se emitió y entregó (`EMITTED`, flujo B2B).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "example": true },
                    "data": {
                      "type": "object",
                      "properties": {
                        "p_code": { "type": "string" },
                        "referenceId": { "type": "string" },
                        "status": { "type": "string", "enum": ["VALIDATED", "EMITTED"] },
                        "titular": { "$ref": "#/components/schemas/Titular" },
                        "certificate": {
                          "type": "object",
                          "description": "Solo presente si `status` es `EMITTED` (partner B2B).",
                          "properties": {
                            "serialNumber": { "type": "string" },
                            "validFrom": { "type": "string", "format": "date-time" },
                            "validTo": { "type": "string", "format": "date-time" },
                            "pfxFileName": { "type": "string" }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Campo requerido faltante o formato inválido.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "401": { "description": "API Key inválida o ausente." },
          "404": { "description": "El código de compra no existe." },
          "406": {
            "description": "Rechazo de negocio: serie requerida y ausente, RUT no coincide con el código de compra, email ya usado por otro titular, validación en curso en otro código de compra, cédula no coincide con Registro Civil, o fecha de vencimiento no coincide.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          },
          "423": { "description": "El código de compra está bloqueado de forma permanente." },
          "429": {
            "description": "Se agotó el límite de intentos de validación de cédula (bloqueo de seguridad), o se superó el rate limit de emisión (10 requests/minuto por partner)."
          }
        }
      }
    },
    "/certificate_claim": {
      "post": {
        "summary": "Obtener el certificado (PFX) — solo la primera vez",
        "description": "Genera y entrega el certificado de un código de compra que ya pasó por `certificate_request` (flujo B2C). Es de una sola vez: si el certificado ya fue reclamado, responde `201` sin volver a generarlo. No aplica a partners B2B, que reciben el certificado automáticamente en `certificate_request`.",
        "operationId": "certificateClaim",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["p_code", "dni", "pin"],
                "properties": {
                  "p_code": { "type": "string" },
                  "dni": { "type": "string", "example": "18925910-7" },
                  "pin": { "type": "string", "pattern": "^\\d{4}$", "description": "El mismo PIN enviado en certificate_request." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Certificado emitido y entregado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "example": true },
                    "data": {
                      "type": "object",
                      "properties": {
                        "p_code": { "type": "string" },
                        "certificate": { "type": "string", "format": "byte", "description": "PFX en base64." },
                        "validFrom": { "type": "string", "format": "date-time" },
                        "validTo": { "type": "string", "format": "date-time" },
                        "serialNumber": { "type": "string" }
                      }
                    }
                  }
                }
              }
            }
          },
          "201": { "description": "El certificado ya había sido reclamado antes — no se regenera. `error: \"ALREADY_ISSUED\"`." },
          "202": { "description": "Hay una validación en curso sobre este código de compra — reintentar en unos segundos." },
          "401": { "description": "API Key inválida o ausente." },
          "404": { "description": "El código de compra no existe, o existe pero todavía no pasó por `certificate_request` (identidad no validada)." },
          "406": { "description": "El RUT/PIN no coinciden con lo generado en `certificate_request`." }
        }
      }
    },
    "/status": {
      "get": {
        "summary": "Consultar el estado de un código de compra",
        "description": "Estado simplificado, sin exponer campos internos — pensado para que un partner integrado vía widget pueda consultar en qué va un usuario sin depender de escuchar eventos del widget en tiempo real.",
        "operationId": "getStatus",
        "parameters": [
          { "name": "p_code", "in": "query", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "example": true },
                    "data": {
                      "type": "object",
                      "properties": {
                        "p_code": { "type": "string" },
                        "status": {
                          "type": "string",
                          "enum": ["NOT_FOUND", "ACTIVE", "VALIDATED", "IN_ONBOARDING", "EMITTED", "EXPIRED", "LOCKED", "CANCELLED"],
                          "description": "En orden de precedencia (el primero que aplica gana): CANCELLED (anulado, gana incluso sobre uno ya emitido) > EXPIRED (se emitió pero ya venció) > EMITTED (certificado entregado) > LOCKED (bloqueado antes de emitirse) > IN_ONBOARDING (a mitad del flujo del widget) > VALIDATED (identidad verificada, falta certificate_claim, solo B2C) > ACTIVE (existe, nadie lo canjeó) > NOT_FOUND (no existe). VALIDATED e IN_ONBOARDING todavía no forman parte del enum purchase_codes.status del modelo unificado hacia el que se está migrando."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Falta el parámetro p_code." },
          "401": { "description": "API Key inválida o ausente." }
        }
      }
    }
  }
}
