Saltar a contenido

API de resolución

Contrato de la API que consulta el GatewayQR en cada pago (SPEC §4). Es la única superficie de contacto entre BeneficiosCenter y el gateway — la ruta caliente descrita en Arquitectura del sistema.

POST /api/v1/beneficios/resolver

Headers: X-Api-Key: <clave>, Content-Type: application/json

Request

{
  "requestId": "GW-20260903-000123",
  "transaction": { "datetime": "2026-09-03T18:42:10-03:00", "amount": 100000.00, "currency": "ARS" },
  "merchant":    { "branchId": "SUC-014", "posId": 7, "salesPointId": 3 },
  "invoice":     { "type": "B", "taxIdKind": "CUIL", "taxId": "27123456784", "extraCustomerTypeId": null }
}
Campo Tipo Descripción
requestId string El trxid que el GatewayQR generó en POST /api/trxid al iniciar el pago. Único por transacción, conocido por POS, gateway y autorizador.
transaction.datetime ISO-8601 con offset Fecha y hora que decide la evaluación, interpretada en America/Argentina/Cordoba (INV-D05). No se usa el reloj del servidor.
transaction.amount decimal Monto del ticket.
transaction.currency string Moneda (ARS).
merchant.branchId string Código de sucursal del comercio. Puede no estar en el catálogo (§ Casos sucios).
merchant.posId / .salesPointId int Identificadores del punto de venta; no filtran beneficios en v1, viajan para auditoría.
invoice.type string A, B o C, tal como los emite el POS. Consumidor final sin identificación viaja como B con taxId vacío.
invoice.taxIdKind string CUIT, CUIL o DNI.
invoice.taxId string Identificación tributaria del comprador. Solo filtro de entrada; nunca dimensión de la respuesta (INV-D03).
invoice.extraCustomerTypeId string o null Código extra opcional que algunos beneficios exigen (codigosExtra).

Un valor de invoice.type o invoice.taxIdKind fuera de los conjuntos esperados no se rechaza: cuenta como "no coincide" en los beneficios que filtran por factura.

Response 200

{
  "requestId": "GW-20260903-000123",
  "evaluatedAt": "2026-09-03T18:42:10.084-03:00",
  "benefits": [
    { "customerTypeCode": "DINI_NEGOCIOS", "paymentMethodId": "990", "type": "DISCOUNT", "percentage": 10.0,
      "discountAmount": 10000.00, "benefitId": 41, "description": "Jueves Dini Negocios 10%",
      "maxDiscountAmount": 15000.00 }
  ],
  "elapsedMs": 4
}
Campo Tipo Descripción
requestId string Eco del request.
evaluatedAt ISO-8601 Momento en que se evaluó, según el reloj del servidor (solo para trazabilidad; no decide nada).
benefits[] array A lo sumo un elemento por tipo de cliente (INV-D01). Lista vacía con 200 cuando nada aplica.
benefits[].customerTypeCode string Código del tipo de cliente ganador.
benefits[].paymentMethodId string Identificador de cuenta que usa el autorizador (990, 991, …).
benefits[].type string Siempre "DISCOUNT" por ahora — modelado como enum del lado del dominio para poder agregar PUNTOS más adelante sin cambiar la forma del contrato (AC-45).
benefits[].percentage decimal Porcentaje del beneficio ganador.
benefits[].discountAmount decimal amount × percentage / 100, redondeado HALF_UP a dos decimales y recortado a maxDiscountAmount cuando existe (AC-45).
benefits[].benefitId / .description Identificación legible del beneficio aplicado, para auditoría y soporte.
benefits[].maxDiscountAmount decimal o null Tope configurado; null si el beneficio no tiene tope.
elapsedMs int Latencia de la resolución, medida en el servidor.

Aguas abajo, en la intención Prisma

Cuando el GatewayQR mapea esta respuesta a benefits_methods_data, type y discountAmount viajan como discount.type y discount.amount: una extensión propuesta por Tipre, no campos del estándar Prisma v27. Ver Contrato con el autorizador.

Errores

Toda respuesta de error usa la forma {"field": "<campo>", "message": "<detalle>"}, salvo que se indique lo contrario.

Status Cuándo field
400 Campo de request inválido (Bean Validation) el campo que falló
400 Body no es JSON válido body
401 X-Api-Key ausente o inválida X-Api-Key
500 Falla inesperada resolviendo internal — nunca el mensaje de la excepción ni el stack (INV-S03)

Los tres primeros nunca llegan a evaluar ningún beneficio (INV-S01 para el 401). El 500 es la única forma que no distingue el detalle real de la falla: es intencional, para no filtrar información interna al gateway.

Reglas del contrato

  • benefits contiene a lo sumo un elemento por tipo de cliente — la regla de oro (INV-D01, INV-D02): los beneficios nunca se suman.
  • Un requestId repetido (reintento del gateway por timeout) se responde normalmente y se audita de nuevo, marcado como repetido. No hay deduplicación.
  • La fecha y hora que decide es transaction.datetime, interpretada en America/Argentina/Cordoba — nunca el reloj del servidor.
  • invoice.* y extraCustomerTypeId son filtros de entrada; nunca aparecen como dimensión de la respuesta.

Desempate

Cuando más de un beneficio aplica al mismo tipo de cliente, gana en este orden:

  1. prioridad numérica más baja (1 gana a 10).
  2. A igual prioridad, porcentaje mayor.
  3. A igual porcentaje, id menor.

El resultado es determinista: dos consultas idénticas contra el mismo snapshot devuelven la misma respuesta (AC-13).

Casos sucios

Caso Comportamiento
Sucursal desconocida No se rechaza. Aplican solo los beneficios sin restricción de sucursal; los restringidos se descartan con motivo SUCURSAL_DESCONOCIDA. Se audita con esa marca.
Tipo de factura no reconocido No se rechaza. Los beneficios que filtran por tipo de factura se descartan con FACTURA_NO_COINCIDE; los que no filtran aplican.
requestId repetido Se responde normalmente y se audita de nuevo con marca REPETIDA.
Base de datos lenta o caída La respuesta no se ve afectada; la auditoría se pierde, contada y alertada (INV-O01, INV-O02).
Sin beneficios configurados o snapshot vacío benefits: [] con 200.

Cómo se evalúa cada beneficio (camino feliz)

  1. Autenticar la API key. Sin clave válida: 401, sin evaluar nada.
  2. Validar el request. Campo inválido: 400 con el nombre del campo.
  3. Tomar la referencia al snapshot ConfiguracionActiva vigente.
  4. Para cada beneficio en estado ACTIVO, evaluar en orden y registrar el primer motivo de descarte:
  5. vigencia: vigenciaDesde <= fecha <= vigenciaHasta
  6. día de semana dentro de diasSemana (vacío = todos)
  7. hora dentro de [horaDesde, horaHasta) (vacío = todo el día)
  8. sucursal: branchId en sucursales (vacío = todas)
  9. monto: amount >= montoMinimo (mayor o igual; sin mínimo = siempre)
  10. tipo de factura: invoice.type en tiposFactura (vacío = todos)
  11. código extra: invoice.extraCustomerTypeId en codigosExtra (vacío = todos; request sin código y beneficio que filtra = descarte CODIGO_EXTRA_NO_COINCIDE)
  12. Agrupar los beneficios que aplican por tipo de cliente y elegir uno por grupo (desempate).
  13. Responder benefits con un elemento por tipo de cliente ganador, más elapsedMs.
  14. Encolar la auditoría (request, response, evaluaciones con motivo) sin bloquear.

Dónde sigue