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.
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¶
benefitscontiene a lo sumo un elemento por tipo de cliente — la regla de oro (INV-D01, INV-D02): los beneficios nunca se suman.- Un
requestIdrepetido (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 enAmerica/Argentina/Cordoba— nunca el reloj del servidor. invoice.*yextraCustomerTypeIdson 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:
prioridadnumérica más baja (1 gana a 10).- A igual prioridad,
porcentajemayor. - A igual porcentaje,
idmenor.
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)¶
- Autenticar la API key. Sin clave válida:
401, sin evaluar nada. - Validar el request. Campo inválido:
400con el nombre del campo. - Tomar la referencia al snapshot
ConfiguracionActivavigente. - Para cada beneficio en estado
ACTIVO, evaluar en orden y registrar el primer motivo de descarte: - vigencia:
vigenciaDesde <= fecha <= vigenciaHasta - día de semana dentro de
diasSemana(vacío = todos) - hora dentro de
[horaDesde, horaHasta)(vacío = todo el día) - sucursal:
branchIdensucursales(vacío = todas) - monto:
amount >= montoMinimo(mayor o igual; sin mínimo = siempre) - tipo de factura:
invoice.typeentiposFactura(vacío = todos) - código extra:
invoice.extraCustomerTypeIdencodigosExtra(vacío = todos; request sin código y beneficio que filtra = descarteCODIGO_EXTRA_NO_COINCIDE) - Agrupar los beneficios que aplican por tipo de cliente y elegir uno por grupo (desempate).
- Responder
benefitscon un elemento por tipo de cliente ganador, máselapsedMs. - Encolar la auditoría (request, response, evaluaciones con motivo) sin bloquear.
Dónde sigue¶
- Cómo el GatewayQR traduce esta respuesta hacia el autorizador, en Contrato con el autorizador.
- Probarlo sin el gateway real, con el Simulador del gateway.
- Todos los criterios que fijan este contrato, en la Matriz de aceptación.