{
  "openapi": "3.1.0",
  "info": {
    "title": "EloPago API (sandbox)",
    "version": "1.0.0",
    "description": "API de cobranzas con distribución (split) en guaraníes. Entorno sandbox: ninguna operación mueve dinero real; los pagos y repasses los ejecuta un proveedor simulado. Montos en enteros PYG; porcentajes en puntos básicos (1 % = 100)."
  },
  "servers": [{ "url": "/api/v1", "description": "Mismo dominio de la plataforma" }],
  "security": [{ "bearer": [] }],
  "components": {
    "securitySchemes": { "bearer": { "type": "http", "scheme": "bearer", "description": "Credencial creada en Configuración y API (ep_test_…)." } },
    "parameters": {
      "IdempotencyKey": { "name": "Idempotency-Key", "in": "header", "required": false, "schema": { "type": "string", "minLength": 8, "maxLength": 80 }, "description": "Misma clave + mismo cuerpo devuelve la respuesta original (cabecera Idempotent-Replayed: true). Otro cuerpo con la misma clave → 409." },
      "Limit": { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 } },
      "Page": { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1, "default": 1 } },
      "ChargeId": { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "example": "cob_ab12cd34ef56gh78" } }
    },
    "schemas": {
      "Error": { "type": "object", "properties": { "error": { "type": "object", "required": ["code", "message"], "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "fields": { "type": "object", "additionalProperties": { "type": "string" } } } } } },
      "Beneficiary": { "type": "object", "properties": { "id": { "type": "string" }, "kind": { "enum": ["PF", "PJ"] }, "name": { "type": "string" }, "email": { "type": "string" }, "document": { "type": "object", "properties": { "type": { "type": "string" }, "last4": { "type": "string" } } }, "verification_status": { "enum": ["incompleto", "en_revision", "info_solicitada", "verificado", "rechazado", "bloqueado"] }, "payout": { "type": ["object", "null"] }, "created_at": { "type": "string" } } },
      "BeneficiaryCreate": { "type": "object", "required": ["kind", "name", "doc_type", "doc_number", "email"], "properties": { "kind": { "enum": ["PF", "PJ"] }, "name": { "type": "string" }, "doc_type": { "enum": ["CI", "RUC", "PASAPORTE", "DNI", "CPF"] }, "doc_number": { "type": "string" }, "email": { "type": "string", "format": "email" }, "phone": { "type": "string" } } },
      "Calculation": { "type": "object", "properties": { "engine": { "type": "string", "example": "split-2.0" }, "strategy": { "enum": ["principal", "prorata"] }, "gross": { "type": "integer" }, "fee": { "type": "object", "properties": { "percentage_bp": { "type": "integer" }, "percentage_amount": { "type": "integer" }, "fixed": { "type": "integer" }, "total": { "type": "integer" } } }, "allocations": { "type": "array", "items": { "type": "object", "properties": { "label": { "type": "string" }, "destination": { "enum": ["company", "beneficiary"] }, "gross_share": { "type": "integer" }, "fee_deduction": { "type": "integer" }, "rounding_adjustment": { "type": "integer" }, "net": { "type": "integer" } } } }, "allocated": { "type": "integer" }, "difference": { "type": "integer" }, "ok": { "type": "boolean" }, "errors": { "type": "array", "items": { "type": "object" } } } },
      "Charge": { "type": "object", "properties": { "id": { "type": "string" }, "status": { "enum": ["borrador", "pendiente", "pagada", "fallida", "vencida", "cancelada", "reembolsada"] }, "reconciliation_status": { "enum": ["no_aplica", "en_curso", "conciliada", "con_pendencias", "divergente"] }, "amount": { "type": "integer" }, "currency": { "const": "PYG" }, "description": { "type": "string" }, "reference": { "type": ["string", "null"] }, "payer": { "type": "object" }, "due_date": { "type": "string", "format": "date" }, "fee_total": { "type": ["integer", "null"] }, "payment_url": { "type": ["string", "null"] }, "paid_at": { "type": ["string", "null"] }, "livemode": { "const": false }, "calculation": { "$ref": "#/components/schemas/Calculation" }, "transfers": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "destination": { "type": "string" }, "amount": { "type": "integer" }, "status": { "enum": ["preparada", "retenida", "enviada", "aceptada", "completada", "rechazada", "sin_respuesta", "anulada"] }, "attempt": { "type": "integer" }, "provider_ref": { "type": ["string", "null"] }, "reason": { "type": ["string", "null"] } } } } } },
      "ChargeCreate": { "type": "object", "required": ["amount", "description", "payer", "due_date", "split_rule"], "properties": { "amount": { "type": "integer", "minimum": 1, "example": 1000000 }, "description": { "type": "string" }, "reference": { "type": "string" }, "payer": { "type": "object", "required": ["name"], "properties": { "name": { "type": "string" }, "email": { "type": "string" } } }, "due_date": { "type": "string", "format": "date" }, "split_rule": { "type": "string", "example": "rul_ab12cd34ef56gh78" }, "publish": { "type": "boolean", "default": false } } }
    },
    "responses": {
      "Unauthorized": { "description": "Credencial ausente o inválida", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Validation": { "description": "Datos inválidos", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "NotFound": { "description": "No existe o pertenece a otra empresa", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "RateLimited": { "description": "Límite por minuto superado", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    }
  },
  "paths": {
    "/beneficiaries": {
      "get": { "summary": "Listar beneficiarios", "parameters": [{ "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/Page" }], "responses": { "200": { "description": "Lista", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Beneficiary" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } } },
      "post": { "summary": "Registrar beneficiario (queda 'incompleto' hasta su verificación)", "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BeneficiaryCreate" } } } }, "responses": { "201": { "description": "Creado", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Beneficiary" } } } }, "422": { "$ref": "#/components/responses/Validation" } } }
    },
    "/split-rules": { "get": { "summary": "Listar reglas de distribución", "responses": { "200": { "description": "Lista" }, "401": { "$ref": "#/components/responses/Unauthorized" } } } },
    "/split/simulate": { "post": { "summary": "Simular la distribución de un monto con una regla y la tarifa vigente", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "required": ["amount", "split_rule"], "properties": { "amount": { "type": "integer" }, "split_rule": { "type": "string" } } } } } }, "responses": { "200": { "description": "Cálculo", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Calculation" } } } }, "422": { "description": "La regla no cierra para ese monto" } } } },
    "/charges": {
      "get": { "summary": "Listar cobranzas", "parameters": [{ "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/Page" }, { "name": "status", "in": "query", "schema": { "type": "string" } }], "responses": { "200": { "description": "Lista" } } },
      "post": { "summary": "Crear cobranza (borrador o publicada)", "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChargeCreate" } } } }, "responses": { "201": { "description": "Creada", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Charge" } } } }, "409": { "description": "Idempotency-Key reutilizada con otro cuerpo" }, "422": { "$ref": "#/components/responses/Validation" }, "429": { "$ref": "#/components/responses/RateLimited" } } }
    },
    "/charges/{id}": { "get": { "summary": "Detalle con cálculo y repasses", "parameters": [{ "$ref": "#/components/parameters/ChargeId" }], "responses": { "200": { "description": "Cobranza", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Charge" } } } }, "404": { "$ref": "#/components/responses/NotFound" } } } },
    "/charges/{id}/publish": { "post": { "summary": "Publicar un borrador (congela regla y tarifa)", "parameters": [{ "$ref": "#/components/parameters/ChargeId" }, { "$ref": "#/components/parameters/IdempotencyKey" }], "responses": { "200": { "description": "Publicada" }, "422": { "description": "No cumple los requisitos (verificación, destinos, regla)" } } } },
    "/charges/{id}/cancel": { "post": { "summary": "Cancelar", "parameters": [{ "$ref": "#/components/parameters/ChargeId" }], "responses": { "200": { "description": "Cancelada" }, "422": { "description": "Estado no cancelable" } } } },
    "/charges/{id}/reconciliation": { "get": { "summary": "Conciliación de la cobranza", "parameters": [{ "$ref": "#/components/parameters/ChargeId" }], "responses": { "200": { "description": "Cobrado, atribuido, instruido, transferido, estado y divergencias" } } } }
  }
}
