PoC de integración entre microservicios con Java 25, Spring Boot 4.1.1 y gRPC. Se expone una API REST de pagos y se delega la autorización a un adquirente independiente mediante HTTP/2 y Protocol Buffers. El foco técnico es el contrato gRPC y su comportamiento bajo concurrencia, errores, reintentos, streaming y conexiones autenticadas.
Se reproducen autorización, rechazo, consulta de decisiones, recepción de lotes y evaluación de riesgo en una conversación bidireccional.
Los importes se expresan en unidades menores: 12500 representa S/ 125,00 cuando se usa PEN. Se usan tokens sintéticos; no se procesan
números de tarjeta. Se aprueba hasta 500000 unidades menores y se rechaza un importe mayor o el token tok_decline. La evaluación de
riesgo no registra autorizaciones.
| Capacidad | Implementación y propósito |
|---|---|
| Contrato primero | Se generan mensajes y stubs desde contracts/src/main/proto/payments.proto. Se versiona el paquete como axiz.payments.v1; se reserva el campo 6. |
| RPC unaria | Authorize: se registra una decisión durable y se devuelve su resultado. |
| Streaming de servidor | History: se transmiten decisiones individualmente con isReady() y setOnReadyHandler(). |
| Streaming de cliente | SubmitBatch: se reciben hasta 100 pagos y se devuelve un resumen al cerrar el envío. |
| Streaming bidireccional | Evaluate: se responde a cada pago durante la conversación, sin esperar a recibir el lote completo. |
| Control de flujo | Se regula la recepción con request(1); se envía únicamente cuando el transporte está listo. Se mantiene como máximo una respuesta pendiente en el servidor dúplex. |
| Plazos | Se exige un deadline en los métodos funcionales; se aplica un plazo total desde el cliente, incluyendo sus reintentos. |
| Cancelación | Se interrumpe el transporte al cancelar o vencer el plazo. Se descarta un lote recibido si se cancela antes de comenzar su procesamiento. |
| Reintentos seguros | Se permiten hasta tres intentos únicamente para Authorize y UNAVAILABLE, con espera exponencial. Se conserva el mismo identificador del pago. |
| Idempotencia | Se usa una clave primaria en PostgreSQL y INSERT ... ON CONFLICT DO NOTHING, seguido de comparación del contenido dentro de una transacción. Se rechaza la reutilización de la clave con otro pago. |
| Transporte | Se reutiliza un canal Netty shaded; se aplican compresión gzip, límites de mensajes y metadatos, keepalive sin llamadas ociosas y cierre ordenado. |
| Resolución y balanceo | Se usa el resolver del canal y la política round_robin; con DNS que devuelve varias direcciones se distribuyen llamadas entre ellas. El despliegue incluido contiene una instancia del adquirente. |
| Seguridad | Se exige mTLS con una CA local, validación del nombre del servidor y credencial Bearer mediante interceptor. También se autentica la API REST. |
| Correlación | Se propaga x-request-id del ingreso REST al contexto y a los metadatos gRPC. Se devuelve en cabeceras o trailers. |
| Observabilidad | Se registran duración, método y código gRPC en Micrometer, tanto en cliente como en servidor. Los identificadores y tokens no se usan como etiquetas. |
| Operación | Se proporcionan el protocolo estándar de salud gRPC, reflection configurable y endpoints de Actuator. No se registran las consultas periódicas de salud. |
| Errores | Se traducen códigos gRPC a HTTP y ProblemDetail; se evita devolver detalles internos de persistencia. |
| Pruebas | Se usan conexiones TCP reales, handshakes mTLS, arranque de Spring y solicitudes HTTP. Se comprueban también cancelación, límites y concurrencia. |
No se aplica reintento automático a streams. Se conserva la identidad de cada pago para que pueda recuperarse un lote con una nueva solicitud explícita. Un plazo vencido no acredita que la autorización haya sido revertida: se repite la solicitud con el mismo contenido e identificador para recuperar una decisión que pudiera haberse confirmado.
flowchart TD
C["Cliente REST o curl"] --> P["payment-service :8080"]
P -->|"gRPC HTTP/2, mTLS y Protobuf"| A["acquirer-service :9090"]
A -->|"JDBC y Flyway"| D["PostgreSQL :5432"]
G["grpcurl"] -->|"mTLS"| A
Se separan los proyectos Maven de ambos servicios. contracts contiene únicamente el contrato y código generado. grpc-support contiene utilidades técnicas de metadatos y métricas; no comparte lógica de negocio ni persistencia.
Se mantienen dos canales activos en la misma instancia de payment-service, ambos conectados al único servidor Spring gRPC del
adquirente. Se selecciona un cliente por solicitud mediante X-Grpc-Client: java o X-Grpc-Client: spring; no se reinicia el proceso
ni se recrean contenedores para alternarlos. Cada solicitud ejecuta un solo RPC funcional con el cliente indicado.
| Cliente | Construcción y seguridad del canal | Ciclo de vida |
|---|---|---|
| gRPC Java | GrpcConfiguration construye NettyChannelBuilder y configura mTLS con GrpcSslContexts. |
RpcChannel administra el cierre del canal Java. |
| Spring gRPC | SpringGrpcConfiguration usa GrpcChannelFactory, el canal lógico acquirer y un bundle SSL de Spring Boot. |
La fábrica administra el cierre del canal Spring. |
Spring gRPC utiliza gRPC Java internamente. Se comparan la configuración y administración de los canales. Se crean dos adaptadores GrpcAcquirerAdapter independientes con los mismos stubs blocking y asíncronos. RoutingAcquirerAdapter implementa el puerto hexagonal y delega al adaptador seleccionado sin duplicar lógica funcional. Ambos clientes soportan los cuatro modos, deadlines, gzip, control de flujo, Bearer, correlación, métricas y reintento exclusivo de Authorize ante UNAVAILABLE. Se reutiliza GrpcChannelPolicy para aplicar los mismos límites y backoff.
RequestFilter valida la selección, la guarda en un contexto acotado a la solicitud y devuelve el cliente utilizado en la cabecera X-Grpc-Client. El contexto se libera en finally; las solicitudes concurrentes no comparten una selección mutable. Un valor de cabecera diferente de java o spring devuelve HTTP 400 antes de invocar gRPC. Si se omite la cabecera, se utiliza el cliente predeterminado de .env; esa configuración no desactiva el otro canal. La selección no cambia las credenciales ni las reglas de autorización.
Los dos canales usan la misma CA y la identidad cliente del despliegue. El bundle PEM de Spring se registra cuando TLS está habilitado; ambos canales admiten el diagnóstico local sin TLS. El servidor permanece exclusivamente con Spring gRPC. Se siguen las APIs oficiales de GrpcChannelFactory y cliente Spring Boot con SSL bundles.
Se consulta GET /actuator/info para comprobar que los dos clientes están disponibles:
{"grpcClients":["java","spring"],"grpcClientDefault":"java"}Se distinguen los RPC de cada canal en las métricas mediante la etiqueta client. La idempotencia se mantiene en el adquirente y PostgreSQL: la misma clave y contenido enviados desde distintos clientes devuelven la misma decisión; un contenido diferente devuelve conflicto.
| Tecnología | Versión | Uso |
|---|---|---|
| Java | 25; validación con Temurin 25.0.4.1 | Runtime, compilación y tipado de ambos servicios. |
| Maven | 3.10.0 | Reactor, generación de contratos, pruebas y empaquetado. |
| Spring Boot | 4.1.1 | Configuración, REST, Actuator y soporte oficial de cliente y servidor gRPC. |
| Spring Framework | 7.0.9 | Versión administrada por Spring Boot; inyección, validación y transacciones. |
| Spring gRPC | 1.1.1 | Integración del servidor y de la alternativa cliente con el ciclo de vida de Spring. |
| gRPC Java | 1.83.1 | Stubs, streams y transporte Netty shaded. |
| Protocol Buffers | 4.35.1 | Mensajes binarios y generación mediante protoc. |
| PostgreSQL | 18.6 | Persistencia transaccional e idempotencia concurrente. |
| Flyway | 12.4.0 | Migración del esquema durante el arranque del adquirente. |
| Spotless y Google Java Format | 3.6.0 y 1.34.1 | Formato del código Java, verificado en mvn verify. |
| Carpeta | Contenido |
|---|---|
payment-service/ |
Proyecto de API REST, casos de uso y cliente gRPC. |
acquirer-service/ |
Proyecto del adquirente gRPC, dominio y adaptador JDBC. |
contracts/ |
Contratos versionados y generación de mensajes y stubs. |
grpc-support/ |
Interceptores y claves de contexto compartidas. |
infrastructura/ |
Compose, Dockerfiles, generación de certificados y ejemplos de request/response. |
datasets/ |
Pagos sintéticos y carga idempotente a través de la API. |
scripts/ |
Compilación, arranque en host, rutas y pruebas reproducibles. |
verification/ |
Salida de compilación y reportes de las pruebas ejecutadas. |
| Clase | Responsabilidad |
|---|---|
PaymentController |
Se valida el ingreso REST y se ejecuta el caso de uso correspondiente. |
PaymentService |
Se aplica el límite de lote y se delega por el puerto del adquirente. |
GrpcAcquirerAdapter |
Se convierten modelos a Protobuf, se aplican deadlines y se coordinan streams con control de flujo. |
GrpcConfiguration |
Se construye y administra un canal compartido con mTLS, retry, balanceo, compresión y límites. |
GrpcConfiguration / SpringGrpcConfiguration |
Se crean simultáneamente el canal Java y el canal administrado por Spring. |
GrpcClientAdapters / RoutingAcquirerAdapter |
Se crean ambos adaptadores y se selecciona uno por solicitud en el puerto hexagonal. |
GrpcChannelPolicy / GrpcClientInfo |
Se unifica la política de transporte y se informan ambos clientes disponibles y el predeterminado. |
RequestFilter / ClientHeaders |
Se autentica el ingreso y se propaga la correlación al RPC. |
RpcGuard / ClientMetrics |
Se validan metadatos del servidor y se registran métricas del transporte. |
AcquirerGrpcAdapter |
Se implementan las cuatro modalidades del contrato y se traducen errores de aplicación. |
PaymentUseCase / Authorization |
Se ejecutan autorización y evaluación; se aplica la política de riesgo sintética. |
JdbcAuthorizationStore |
Se registra o recupera una decisión sin duplicarla, incluso bajo solicitudes concurrentes. |
ApiErrors |
Se transforman fallos gRPC a respuestas REST estructuradas. |
Se valida todo el lote recibido antes de escribir. El lote no constituye una transacción única: cada pago se confirma de forma independiente. Si aparece un conflicto posterior, los pagos previos pueden permanecer confirmados. Se recupera el resultado repitiendo los pagos con sus mismas claves; no se presupone entrega exactamente una vez por parte del transporte.
| Paso | Endpoint REST | Función | RPC o mecanismo técnico |
|---|---|---|---|
| Comprobación | GET :8080/actuator/health |
Se comprueba la API de pagos. | Salud HTTP. |
| Comprobación | GET :8081/actuator/health |
Se comprueba el adquirente y su base. | Salud HTTP y JDBC. |
| Evaluación opcional | POST :8080/api/payments/evaluate |
Se evalúan hasta 100 pagos sin autorizarlos. | Evaluate, streaming bidireccional. |
| Autorización | POST :8080/api/payments |
Se obtiene una decisión durable para un pago. | Authorize, RPC unaria idempotente. |
| Autorización por lote | POST :8080/api/payments/batch |
Se autorizan hasta 100 pagos. | SubmitBatch, streaming de cliente. |
| Consulta | GET :8080/api/payments?merchantId=...&limit=50 |
Se recuperan decisiones anteriores. | History, streaming de servidor. |
| Inspección | GET :8080/actuator/metrics/axiz.grpc.client |
Se consultan duración y cantidad de RPC del cliente. | Micrometer; aparece después de realizar llamadas. |
| Inspección | GET :8081/actuator/metrics/axiz.grpc.server |
Se consultan métricas de los RPC funcionales. | Micrometer. |
| Operación | GET :8080/actuator/info, GET :8081/actuator/info |
Se consultan información del proceso y disponibilidad de clientes. | Actuator; en payment se informan grpcClients y grpcClientDefault y en acquirer se conserva la respuesta sin información adicional. |
| Operación | GET :8080/actuator/metrics, GET :8081/actuator/metrics |
Se enumeran métricas. | Actuator. |
| Código gRPC | HTTP | Interpretación |
|---|---|---|
INVALID_ARGUMENT |
400 | Datos o metadatos inválidos. |
UNAUTHENTICATED / PERMISSION_DENIED |
401 / 403 | Credencial ausente, inválida o acceso rechazado. |
NOT_FOUND |
404 | Recurso no encontrado. |
ALREADY_EXISTS / FAILED_PRECONDITION |
409 | Conflicto de idempotencia o estado. |
RESOURCE_EXHAUSTED |
429 | Límite del transporte excedido. |
CANCELLED |
408 | RPC cancelada. |
UNAVAILABLE / DEADLINE_EXCEEDED |
503 / 504 | Integración no disponible o plazo agotado. |
| Otros fallos gRPC | 502 | Error de integración. |
Un rechazo de riesgo devuelve HTTP 200 y DECLINED: se completó correctamente la evaluación funcional.
cp .env.example .envEn una terminal se mantiene PostgreSQL en primer plano:
docker compose --env-file .env -f infrastructura/docker-compose.yml up postgresEn otra terminal se compilan y arrancan ambos servicios, se generan los certificados y se carga el dataset:
bash scripts/run-host.shSe conserva el control de ambos procesos y se cierran al interrumpir el script. La generación de certificados y la carga pueden repetirse. El arranque del adquirente aplica Flyway antes de atender solicitudes. No se ejecuta una segunda inicialización SQL en Compose.
Para depurar desde un IDE, se ejecuta primero mvn install en la raíz y se importan los proyectos Maven. Se generan los certificados
con bash infrastructura/generate-certs.sh y se exporta el archivo de configuración:
set -a
source .env
set +a
mvn -B install
bash infrastructura/generate-certs.sh
bash scripts/routes.shSe arrancan las clases pe.axiz.acquirer.Application y pe.axiz.payment.Application con directorio de trabajo en la raíz. Después se ejecuta bash datasets/seed.sh. No se mezcla este modo con aplicaciones ya levantadas por Compose, pues se usan los mismos puertos.
Se construye primero la imagen base de Java y Maven, con dependencias y herramientas resueltas. Se usan additional_contexts para que
BuildKit prepare esa base antes de compilar cada aplicación. Se conserva la base con una etiqueta estable y una capa de dependencias que
solo cambia al modificar los POM o el Dockerfile base. Al modificar código se recompilan las aplicaciones sin reinstalar sus herramientas. La imagen de ejecución se reutiliza y el contenedor corre como usuario sin privilegios.
Se construye y levanta todo con un comando, en primer plano:
docker compose --env-file .env -f infrastructura/docker-compose.yml --profile app up --buildSe ejecutan certificados, migración Flyway, ambos servicios y carga inicial. El contenedor seed debe terminar con código 0; al finalizar la carga se imprimen las rutas disponibles. Los servicios de larga duración permanecen en primer plano. Los healthchecks redirigen su salida y no se registran como accesos de aplicación.
En las siguientes ejecuciones se usan las imágenes existentes, sin construirlas:
docker compose --env-file .env -f infrastructura/docker-compose.yml --profile app up --no-buildLa primera ejecución requiere acceso a los registros de contenedores y a Maven Central. Se mantienen datos y certificados en volúmenes. Se detiene y retira el despliegue sin eliminar esos datos con:
docker compose --env-file .env -f infrastructura/docker-compose.yml --profile app downSe reconstruye la versión actualizada una vez con el comando Compose de la sección anterior. Después se pueden usar ambos clientes desde la misma instancia. También se aplica esta selección cuando los servicios se ejecutan en el host.
Se autoriza un pago primero con Java y después con Spring. La segunda solicitud comprueba la idempotencia compartida:
set -a
source .env
set +a
for client in java spring; do
curl -i -sS http://localhost:8080/api/payments \
-H "Authorization: Bearer $API_TOKEN" \
-H "X-Grpc-Client: $client" \
-H 'Content-Type: application/json' \
-d '{"paymentId":"dual-client-demo","merchantId":"merchant_demo","amountMinor":12500,"currency":"PEN","token":"tok_demo"}'
doneSe espera HTTP 200 y la misma decisión APPROVED en ambas llamadas. Se devuelve X-Grpc-Client: java en la primera y
X-Grpc-Client: spring en la segunda. Las cabeceras también se admiten en historial, lote y evaluación. Todos los ejemplos curl
posteriores pueden ejecutarse con cualquiera de las dos cabeceras.
Se prueba la coexistencia con un solo comando, sin Maven, grpcurl ni operaciones Docker:
bash scripts/test-clients.shSe ejecuta la suite funcional completa con cada cliente. Después se envían 24 solicitudes concurrentes alternando Java y Spring sobre la misma clave, se comprueban replay y conflicto entre clientes, se rechaza una selección inválida y se verifica un único registro persistente desde ambos historiales. Se requieren curl, Python 3 y el despliegue actualizado en ejecución. Después se ejecuta el reporte de rendimiento de la sección siguiente. Se conserva scripts/test-clients-compose.sh como entrada compatible para el mismo comando; ya no recrea contenedores ni restaura un cliente al terminar.
Para probar una sola alternativa sobre el despliegue existente:
bash scripts/test-live.sh java
bash scripts/test-live.sh springSin argumento, scripts/test-live.sh prueba el cliente predeterminado. scripts/grpc-test.sh inspecciona directamente el adquirente; no pasa por ninguno de los clientes REST.
scripts/test-clients.sh genera automáticamente el reporte después de comprobar el funcionamiento y la coexistencia. También se puede ejecutar únicamente la medición sobre los servicios ya desplegados:
bash scripts/benchmark.shSe imprime una tabla por cliente y operación, el tiempo acumulado de los bloques medidos y la diferencia porcentual de Spring respecto de Java. Se guardan archivos reports/grpc-clients-<fecha-UTC>.json y .csv con un nombre único por ejecución. JSON conserva la configuración, los resultados, los códigos RPC, aprobaciones, rechazos funcionales, categorías de error y comparaciones; CSV permite analizar las filas en una hoja de cálculo. No se incluyen credenciales ni datos de tarjetas. Las opciones y sus límites se describen exclusivamente en .env.example; si se conserva un .env anterior, se aplican los valores predeterminados de las opciones nuevas.
| Escenario | Operación gRPC | Trabajo medido |
|---|---|---|
authorize |
Unary Authorize |
Se autorizan pagos con claves nuevas; incluye escritura en PostgreSQL. |
replay |
Unary Authorize |
Se repite un pago precargado; se mide el recorrido idempotente. |
history |
Server streaming History |
Se recibe un historial precargado de tamaño fijo. |
batch |
Client streaming SubmitBatch |
Se envía un lote de tamaño fijo con claves nuevas y se reciben sus decisiones. |
evaluate |
Bidirectional streaming Evaluate |
Se evalúa un conjunto del mismo tamaño que el lote sin persistir autorizaciones. |
| Métrica | Interpretación |
|---|---|
| Tiempo acumulado por cliente | Suma de la duración real de sus bloques medidos; se excluyen preparación, calentamiento, consulta de métricas y espera entre bloques. |
| Media, mínimo, máximo y desviación estándar | Latencia HTTP observada, incluida la serialización y lectura de la respuesta. Se conservan también los tiempos de intentos fallidos. |
| p50, p95 y p99 | Percentiles calculados por interpolación sobre las muestras del cliente y escenario. Se informa el tamaño de muestra para interpretar la cola de latencia. |
| Solicitudes/s | Intentos divididos entre la duración real de los bloques; incluye concurrencia. |
| Solicitudes exitosas/s | Respuestas válidas divididas entre la duración de los bloques. |
| Unidades/s | Pagos procesados por segundo; en historial, decisiones recibidas por segundo. Un lote representa varias unidades y un solo RPC. |
| Errores y porcentaje | Fallos HTTP, transporte o contrato de respuesta. No se confunden errores técnicos con rechazos funcionales de pagos. |
| RPC: cantidad y media | Diferencia de COUNT y TOTAL_TIME de Micrometer antes y después de medir; se filtra por cliente y método. Se excluye el calentamiento. |
| Códigos RPC | Cantidad de llamadas terminadas por código gRPC durante la ventana medida. |
| Diferencia Spring/Java | Variación porcentual por escenario. En latencia, un valor positivo representa mayor tiempo; en rendimiento representa más solicitudes exitosas/s. |
Se ejecutan primero solicitudes de calentamiento para estabilizar canales y conexiones. Se distribuye la misma cantidad de muestras entre bloques y se alterna su orden con una semilla reproducible. Cada bloque utiliza un número fijo de trabajadores; se mide la latencia desde que un trabajador inicia la solicitud, sin incluir su espera en la cola. El rendimiento utiliza el tiempo real del bloque, no la suma de latencias concurrentes.
La latencia REST incluye HTTP, validación, mapeo, gRPC y el trabajo del adquirente. El temporizador RPC mide una llamada lógica desde el interceptor cliente hasta su cierre, incluidos reintentos; no cuenta cada intento interno por separado. Los canales gRPC se reutilizan, mientras el medidor HTTP abre conexiones mediante la biblioteca estándar de Python. No se atribuye toda la diferencia observada a Spring: ambos clientes comparten JVM, servidor, base de datos y recursos. Los percentiles de muestras pequeñas son exploratorios; se repite la medición antes de establecer conclusiones de rendimiento.
Se utilizan identificadores únicos para evitar replays accidentales en las operaciones de escritura. Se precargan solo el pago de replay y el historial del benchmark mediante la API. Los pagos de autorización y lote quedan persistidos para su revisión; no se eliminan registros ajenos. Se ejecuta la comparación sin tráfico adicional ni reinicios durante la ventana. Si los contadores RPC no coinciden con los intentos HTTP, se marca la comparación como inválida, se conserva el reporte y el script termina con código distinto de cero. También se devuelve un código de fallo si existen errores técnicos en las muestras.
Se verifican los cálculos y la exportación del reporte con pruebas Python sobre un servidor HTTP controlado:
python3 -m unittest discover -s scripts/tests -vEstas pruebas comprueban percentiles, rendimiento exitoso, separación de rechazos funcionales y errores técnicos, reinicios de contadores, validación de configuración y generación de JSON/CSV sin incluir secretos. Se ejecutan también dentro de scripts/test-all.sh.
Se incluye datasets/payments.json con una aprobación y un rechazo. Se carga mediante el RPC de lote, a través de REST, para probar el mismo camino que se usa con las solicitudes funcionales. Flyway crea exclusivamente el esquema; el dataset no duplica las migraciones.
set -a
source .env
set +a
bash datasets/seed.shSe espera una lista con seed-approved / APPROVED y seed-declined / DECLINED. La repetición devuelve las decisiones originales y no crea filas adicionales.
Se mantienen ambos servicios en ejecución y se carga .env en la terminal de pruebas:
set -a
source .env
set +a
curl -fsS http://localhost:8080/actuator/health
curl -fsS http://localhost:8081/actuator/healthSe espera {"status":"UP",...} en ambos casos.
Se autoriza S/ 125,00 y se verifica la traducción REST → Protobuf → JDBC. El request se conserva en infrastructura/requests/authorize.json.
curl --fail-with-body -sS http://localhost:8080/api/payments \
-H "Authorization: Bearer $API_TOKEN" \
-H 'X-Request-Id: prueba-unaria' \
-H 'Content-Type: application/json' \
--data-binary @infrastructura/requests/authorize.jsonRespuesta esperada:
{"paymentId":"pay-demo","status":"APPROVED","reason":"AUTHORIZED","amountMinor":12500,"currency":"PEN"}Se repite el mismo comando para comprobar el replay sin duplicación. Se cambia el importe manteniendo la clave para comprobar el conflicto:
curl -sS -i http://localhost:8080/api/payments \
-H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
--data '{"paymentId":"pay-demo","merchantId":"merchant_demo","amountMinor":12501,"currency":"PEN","token":"tok_demo"}'Se espera HTTP 409 y un cuerpo ProblemDetail con grpcStatus: "ALREADY_EXISTS".
Se envía un pago que excede el límite sintético de riesgo:
curl --fail-with-body -sS http://localhost:8080/api/payments \
-H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
--data '{"paymentId":"pay-declined","merchantId":"merchant_demo","amountMinor":999999,"currency":"PEN","token":"tok_demo"}'Respuesta esperada:
{"paymentId":"pay-declined","status":"DECLINED","reason":"RISK_POLICY","amountMinor":999999,"currency":"PEN"}Se envían los dos pagos del dataset como mensajes gRPC independientes y se obtiene el resumen al cerrar el envío:
curl --fail-with-body -sS http://localhost:8080/api/payments/batch \
-H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
--data-binary @datasets/payments.jsonRespuesta esperada:
[{"paymentId":"seed-approved","status":"APPROVED","reason":"AUTHORIZED","amountMinor":12500,"currency":"PEN"},{"paymentId":"seed-declined","status":"DECLINED","reason":"RISK_POLICY","amountMinor":999999,"currency":"PEN"}]Se usan identificadores nuevos para comprobar evaluaciones sin persistencia. Se alternan mensajes y respuestas en el enlace gRPC:
curl --fail-with-body -sS http://localhost:8080/api/payments/evaluate \
-H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
--data '{"payments":[{"paymentId":"eval-only","merchantId":"merchant_demo","amountMinor":12500,"currency":"PEN","token":"tok_demo"}]}'Respuesta esperada:
[{"paymentId":"eval-only","status":"APPROVED","reason":"AUTHORIZED","amountMinor":12500,"currency":"PEN"}]Se recupera el historial del comercio. Se comprueba que aparecen las autorizaciones, pero no eval-only:
curl --fail-with-body -sS 'http://localhost:8080/api/payments?merchantId=merchant_demo&limit=50' \
-H "Authorization: Bearer $API_TOKEN"Se espera una lista de decisiones con seed-approved, seed-declined y los pagos autorizados durante las pruebas. El orden corresponde al momento en que se registraron. limit=0 devuelve HTTP 400.
Se envía un importe inválido y se realiza una solicitud sin credencial:
curl -sS -i http://localhost:8080/api/payments \
-H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
--data '{"paymentId":"invalid","merchantId":"merchant_demo","amountMinor":0,"currency":"PEN","token":"tok_demo"}'
curl -sS -i 'http://localhost:8080/api/payments?merchantId=merchant_demo'Se esperan HTTP 400 y HTTP 401 respectivamente. No se registran decisiones para esas solicitudes.
Después de las llamadas anteriores se inspeccionan duración, cantidad, método y estado:
curl -fsS http://localhost:8080/actuator/metrics/axiz.grpc.client
curl -fsS http://localhost:8081/actuator/metrics/axiz.grpc.serverSe espera un objeto de métricas con COUNT, TOTAL_TIME, MAX y etiquetas disponibles de método y estado. No se imprimen tokens ni identificadores de pago en estas métricas.
Se requiere grpcurl instalado y los certificados cliente disponibles en el host.
Se inspecciona reflection, se autoriza un pago, se transmite el historial, se envía un lote, se intercambian evaluaciones y se consulta salud. Se usan certificados, credencial y deadline en cada llamada. En el formato JSON de Protobuf, int64 aparece como cadena; por eso el script usa "amountMinor":"12500".
En el modo Compose, se copian los certificados necesarios para la inspección desde el volumen del adquirente:
mkdir -p infrastructura/certs
docker compose --env-file .env -f infrastructura/docker-compose.yml --profile app cp acquirer:/certs/ca.crt infrastructura/certs/ca.crt
docker compose --env-file .env -f infrastructura/docker-compose.yml --profile app cp acquirer:/certs/client.crt infrastructura/certs/client.crt
docker compose --env-file .env -f infrastructura/docker-compose.yml --profile app cp acquirer:/certs/client.key infrastructura/certs/client.key
chmod 600 infrastructura/certs/client.key
bash scripts/grpc-test.shSe observa el intercambio dúplex interactivo sustituyendo la entrada printf del script por líneas JSON introducidas en stdin de grpcurl -d @; se recibe una respuesta por cada mensaje antes del cierre del envío.
Para probar únicamente los servicios desplegados, sin compilar ni descargar dependencias Java en el host, se ejecuta bash scripts/test-live.sh. Para probar ambos clientes y su coexistencia se utiliza bash scripts/test-clients.sh. La compilación y las pruebas Java utilizan Maven y su caché del host, independiente de la caché de la imagen base.
Con los servicios en ejecución, se ejecutan todas las pruebas Java y las suites funcionales de ambos clientes, incluida la concurrencia mixta, con un único comando:
bash scripts/test-all.shSe comprueban autorización, rechazo, replay, conflicto, credenciales, validación, 24 solicitudes concurrentes sobre la misma clave, lote, evaluación sin escritura e historial. Se generan identificadores únicos en cada ejecución para evitar contaminación entre pruebas. El recorrido funcional exige PostgreSQL real y los servicios levantados.
Para compilar, verificar formato y ejecutar las pruebas de transporte y Spring sin levantar la infraestructura:
bash scripts/build.shSe generan acquirer-service/target/acquirer-service-1.0.0.jar y payment-service/target/payment-service-1.0.0.jar. Se incluyen pruebas de ambos clientes, TCP real y mTLS, generación de certificados locales, límites de flujo y lote, plazo agotado, cancelación, reintento seguro, exclusión de reintentos para errores de validación, concurrencia, traducción REST y arranque del servidor gRPC de Spring. Para aplicar el formato antes de la verificación se usa mvn spotless:apply.