Skip to content
Closed
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Yape Code Challenge :rocket:
# Yape Code Challenge :rocket: ALO

Our code challenge will let you marvel us with your Jedi coding skills :smile:.

Expand Down
48 changes: 39 additions & 9 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,55 @@
version: "3.7"
version: "3.8"

services:
postgres:
image: postgres:14
image: postgres:14-alpine
ports:
- "5432:5432"
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=transaction_db
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped

zookeeper:
image: confluentinc/cp-zookeeper:5.5.3
image: confluentinc/cp-zookeeper:7.3.0
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
healthcheck:
test: echo srvr | nc zookeeper 2181 || exit 1
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped

kafka:
image: confluentinc/cp-enterprise-kafka:5.5.3
depends_on: [zookeeper]
image: confluentinc/cp-kafka:7.3.0
depends_on:
- zookeeper
ports:
- "9092:9092"
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_BROKER_ID: 1
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_JMX_PORT: 9991
ports:
- 9092:9092
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
healthcheck:
test: kafka-topics --bootstrap-server kafka:29092 --list || exit 1
interval: 30s
timeout: 10s
retries: 5
restart: unless-stopped

volumes:
postgres_data:
37 changes: 37 additions & 0 deletions ms-antifraude/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// @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,
},
ecmaVersion: 5,
sourceType: 'module',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'prettier/prettier': 'off'
},
},
);
8 changes: 8 additions & 0 deletions ms-antifraude/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
}
}
78 changes: 78 additions & 0 deletions ms-antifraude/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"name": "ms-antifraud-rules",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/microservices": "^11.0.21",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^11.0.0",
"kafkajs": "^2.2.4",
"pg": "^8.15.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.22"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^15.14.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
49 changes: 49 additions & 0 deletions ms-antifraude/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
1. Arquitectura basada en reglas
He implementado un sistema de reglas flexible y extensible:

Motor de reglas (RuleEngine): Coordina la ejecución de múltiples reglas de validación
Reglas específicas:

AmountRule: Valida los montos de las transacciones
AccountRule: Verifica cuentas bloqueadas y validaciones de cuentas
TransferTypeRule: Aplica restricciones según el tipo de transferencia



Esta arquitectura te permite agregar fácilmente nuevas reglas sin modificar el código existente.
2. Mejoras en manejo de eventos

Control de errores robusto en la recepción y emisión de eventos
Formato consistente para eventos de respuesta
Logging detallado de todas las operaciones

3. DTOs y validación

DTOs bien definidos con validaciones usando class-validator
Enumeraciones para estados de transacción para evitar errores tipográficos
Interfaces claras entre componentes

4. Observabilidad

Logger estructurado en todos los componentes
Trazabilidad de transacciones a través del sistema
Mensajes de error descriptivos

5. Configuración centralizada

Uso de ConfigService para acceder a variables de entorno
Parámetros configurables (como límites de montos)

Integración con el microservicio de transacciones
El servicio de antifraud ahora se conecta adecuadamente con el microservicio de transacciones:

Recibe eventos transaction-created con los detalles de la transacción
Aplica reglas de negocio para validar la transacción
Emite eventos transaction-validated con el resultado de la validación

Esta arquitectura es mucho más mantenible y escalable que la implementación original, permitiendo:

Agregar nuevas reglas fácilmente
Configurar parámetros sin cambiar código
Manejar errores de forma robusta
Facilitar las pruebas unitarias
27 changes: 27 additions & 0 deletions ms-antifraude/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ValidatorModule } from './validator/validator.module';
import { LoggerModule } from 'nestjs-pino';

@Module({
imports: [
// Configuración centralizada
ConfigModule.forRoot({
isGlobal: true,
}),

// Logging estructurado
LoggerModule.forRoot({
pinoHttp: {
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty' }
: undefined,
level: process.env.LOG_LEVEL || 'info',
},
}),

// Módulos de la aplicación
ValidatorModule,
],
})
export class AppModule {}
50 changes: 50 additions & 0 deletions ms-antifraude/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { ValidationPipe, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
const logger = new Logger('Bootstrap');

// Kafka brokers desde configuración
const kafkaBrokers = configService.get<string>('KAFKA_BROKERS', 'localhost:9092').split(',');

// Validación global
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);

// Configurar microservicio
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.KAFKA,
options: {
client: {
clientId: 'antifraud-consumer',
brokers: kafkaBrokers,
},
consumer: {
groupId: 'antifraud-consumer-group',
},
},
});

// Iniciar microservicios
await app.startAllMicroservices();
logger.log(`Microservicio Kafka iniciado con brokers: ${kafkaBrokers.join(',')}`);

// Puerto HTTP desde configuración
const port = configService.get<number>('PORT', 3001);

// Iniciar aplicación HTTP
await app.listen(port);
logger.log(`Aplicación iniciada en: ${await app.getUrl()}`);
}

bootstrap();
20 changes: 20 additions & 0 deletions ms-antifraude/src/validator/dto/transaction-validated.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { IsEnum, IsNotEmpty, IsString, IsUUID } from 'class-validator';

export enum TransactionStatus {
PENDING = 'PENDING',
APPROVED = 'APPROVED',
REJECTED = 'REJECTED'
}

export class TransactionValidatedDto {
@IsNotEmpty()
@IsUUID(4, { message: 'transactionExternalId debe ser un UUID válido' })
transactionExternalId: string;

@IsNotEmpty()
@IsEnum(TransactionStatus, { message: 'Estado debe ser APPROVED o REJECTED' })
status: TransactionStatus;

@IsString()
reason?: string;
}
17 changes: 17 additions & 0 deletions ms-antifraude/src/validator/dto/validator.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { IsNotEmpty, IsNumber, IsPositive, IsUUID } from 'class-validator';

export class ValidatorDto {
@IsNotEmpty()
@IsUUID(4, { message: 'transactionExternalId debe ser un UUID válido' })
transactionExternalId: string;

@IsNotEmpty()
@IsNumber()
@IsPositive({ message: 'El valor debe ser un número positivo' })
value: number;

// Campos adicionales que podrían ser útiles para reglas más complejas
accountExternalIdDebit?: string;
accountExternalIdCredit?: string;
tranferTypeId?: number;
}
Loading