Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
version: '3.8'

services:
postgres:
image: postgres:16-alpine
container_name: yape-postgres
environment:
POSTGRES_USER: yape
POSTGRES_PASSWORD: yape_secret
POSTGRES_DB: payments
ports:
- '5432:5432'
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U yape -d payments']
interval: 5s
timeout: 3s
retries: 5

zookeeper:
image: confluentinc/cp-zookeeper:7.6.0
container_name: yape-zookeeper
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
ports:
- '2181:2181'

kafka:
image: confluentinc/cp-kafka:7.6.0
container_name: yape-kafka
depends_on:
- zookeeper
ports:
- '9092:9092'
- '29092:29092'
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'false'
healthcheck:
test: ['CMD', 'kafka-topics', '--bootstrap-server', 'localhost:9092', '--list']
interval: 10s
timeout: 5s
retries: 5

kafka-init:
image: confluentinc/cp-kafka:7.6.0
container_name: yape-kafka-init
depends_on:
kafka:
condition: service_healthy
entrypoint: ['/bin/sh', '-c']
command: |
"
echo 'Creating Kafka topics...'
kafka-topics --bootstrap-server kafka:29092 --create --if-not-exists --topic payment.created.v1 --partitions 3 --replication-factor 1
kafka-topics --bootstrap-server kafka:29092 --create --if-not-exists --topic payment.settled.v1 --partitions 3 --replication-factor 1
kafka-topics --bootstrap-server kafka:29092 --create --if-not-exists --topic payment.failed.v1 --partitions 3 --replication-factor 1
kafka-topics --bootstrap-server kafka:29092 --create --if-not-exists --topic payment.created.v1.dlt --partitions 1 --replication-factor 1
echo 'Topics created successfully.'
kafka-topics --bootstrap-server kafka:29092 --list
"

volumes:
postgres_data:
18 changes: 18 additions & 0 deletions payment-service/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=yape
DB_PASSWORD=yape_secret
DB_NAME=payments
KAFKA_BROKER=localhost:9092
KAFKA_CLIENT_ID=payment-service
MAX_RETRIES=3
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=yape
DATABASE_PASSWORD=yape_secret
DATABASE_NAME=payments

KAFKA_BROKERS=localhost:9092
KAFKA_CLIENT_ID=payment-service

MAX_RETRIES=3
47 changes: 47 additions & 0 deletions payment-service/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# compiled output
/dist
/node_modules
/build

# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# OS
.DS_Store

# Tests
/coverage
/.nyc_output

# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

# dotenv
.env

# temp files
*.swp
*.swo
*~

# TypeScript incremental
*.tsbuildinfo
4 changes: 4 additions & 0 deletions payment-service/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
159 changes: 159 additions & 0 deletions payment-service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Payment Settlement Pipeline

## Requisitos previos

- **Node.js 20+** y npm
- **Docker** y **Docker Compose**

## Cómo ejecutar

### 1. Instalar dependencias

```bash
cd payment-service
npm install
```

### 2. Levantar infraestructura

Desde la raíz del repositorio:

```bash
docker compose up -d
```

Verificar que los contenedores estén saludables:

```bash
docker ps
```

Deben aparecer: `yape-postgres`, `yape-kafka`, `yape-zookeeper` con estado `healthy` o `Up`.

### 3. Iniciar la aplicación NestJS

```bash
cd payment-service
npm run start:dev
```

Esperar hasta ver en consola:

```
All Kafka consumers started.
Payment service running on port 3000
```

### 4. Iniciar el Outbox Relay (en otra terminal)

```bash
cd payment-service
npm run relay
```

Debe mostrar:

```
[OutboxRelay] Connected to PostgreSQL.
[OutboxRelay] Connected to Kafka.
```

---

## Validación de los entregables

### 1. Transactional Outbox — Crear un pago

```bash
# PowerShell
$body = '{"amount": 150.50, "currency": "PEN", "sourceCountry": "PE", "destinationCountry": "MX", "senderId": "user-001", "receiverId": "user-002"}'
Invoke-RestMethod -Uri http://localhost:3000/payments -Method POST -ContentType "application/json" -Body $body | ConvertTo-Json
```

```bash
# curl
curl -X POST http://localhost:3000/payments \
-H "Content-Type: application/json" \
-d '{"amount": 150.50, "currency": "PEN", "sourceCountry": "PE", "destinationCountry": "MX", "senderId": "user-001", "receiverId": "user-002"}'
```

**Qué validar:**

- La respuesta retorna `"status": "pending"` con un `id` UUID.
- En la terminal de NestJS aparece: `Payment <id> created with outbox entry. Awaiting relay.`
- En la terminal del relay aparece: `Published event <id> (payment.created) to payment.created.v1`
- El pago y el outbox entry se escriben en una sola transacción de base de datos. Kafka **nunca** se llama dentro de la transacción.

### 2. Consumers idempotentes — Verificar procesamiento

En la terminal de NestJS deben aparecer los logs de los consumers:

```
Payment <id>: risk score = X.XX
Payment <id>: fraud check passed.
Ledger: DEBIT sender=user-001 amount=150.5 PEN
Ledger: CREDIT receiver=user-002 amount=150.5 PEN
Notification sent for payment <id>
```

**Qué validar:**

- Enviar el mismo evento dos veces no produce efectos duplicados. La idempotencia está en el lado del consumidor, usando la tabla `processed_events` con clave `(eventId, consumerName)`.
- Si el relay publica el mismo evento más de una vez, los consumers lo detectan y muestran: `Event <id> already processed by <Consumer>. Skipping.`

### 3. DLT Handler — Verificar manejo de fallos

Los consumers reintentan hasta 3 veces. Si agotan los reintentos, emiten un evento compensatorio al Dead Letter Topic (`payment.created.v1.dlt`). Los mensajes fallidos nunca se pierden silenciosamente.

**Qué validar:**

- En caso de fallo, la terminal de NestJS muestra: `Emitting to DLT: payment <id>, reason: ...`
- El campo `consumerAcks` del pago refleja `"failed"` para el consumer que falló.

### 4. Status query endpoint — Consultar estado del pago

```bash
# PowerShell
Invoke-RestMethod -Uri http://localhost:3000/payments/<payment-id> -Method GET | ConvertTo-Json -Depth 5
```

```bash
# curl
curl http://localhost:3000/payments/<payment-id>
```

**Qué validar:**

- Inmediatamente después del POST, el status es `"pending"`.
- Después de ~3 segundos, el status cambia a `"settled"` (si ambos consumers fueron exitosos) o `"failed"` (si alguno falló).
- La respuesta incluye `consumerAcks` con el estado individual de cada consumer (`fraud`, `ledger`).
- El campo `_consistency` documenta explícitamente que el endpoint es eventualmente consistente.

Ejemplo de respuesta exitosa:

```json
{
"id": "e1a7d86f-...",
"status": "settled",
"consumerAcks": {
"fraud": "success",
"ledger": "success"
},
"_consistency": {
"model": "eventual",
"note": "Status reflects the last known state. There may be a delay between event processing and status update."
}
}
```

---

## Detener el entorno

```bash
# Detener contenedores
docker compose down

# Detener contenedores y eliminar volúmenes (empezar limpio)
docker compose down -v
```
35 changes: 35 additions & 0 deletions payment-service/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';

export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'commonjs',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
"prettier/prettier": ["error", { endOfLine: "auto" }],
},
},
);
8 changes: 8 additions & 0 deletions payment-service/nest-cli.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
Loading