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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,6 @@ dist

# TernJS port file
.tern-port

#package-lock.json
package-lock.json
174 changes: 120 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,82 +1,148 @@
# Yape Code Challenge :rocket:
# app-nodejs-codechallenge

Our code challenge will let you marvel us with your Jedi coding skills :smile:.
Este repositorio es un microservicio en NestJS para manejar transacciones financieras. Usa PostgreSQL para persistencia y Kafka para la validación asíncrona (anti-fraud).

Don't forget that the proper way to submit your work is to fork the repo and create a PR :wink: ... have fun !!
Este README describe cómo levantar el entorno, cómo usar los servicios (endpoints) y cómo comprobar el flujo de mensajes con Kafka.

- [Problem](#problem)
- [Tech Stack](#tech_stack)
- [Send us your challenge](#send_us_your_challenge)
## Resumen técnico
- Framework: NestJS
- Persistencia: PostgreSQL (TypeORM)
- Mensajería: Kafka
- Arquitectura: monolito modular con flujo event-driven para validación anti-fraud

# Problem
## Requisitos
- Docker & Docker Compose
- Node 18+ y npm o yarn

Every time a financial transaction is created it must be validated by our anti-fraud microservice and then the same service sends a message back to update the transaction status.
For now, we have only three transaction statuses:
## Configuración (.env)
Crear un archivo `.env` en la raíz con las variables de Postgres:

<ol>
<li>pending</li>
<li>approved</li>
<li>rejected</li>
</ol>
```
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=postgres
```

Nota: la app por defecto usa `localhost:9092` como broker Kafka. Si ejecutas la app dentro de Docker, usa `kafka:29092` como broker.

## Levantar el entorno

Every transaction with a value greater than 1000 should be rejected.
1. Instala dependencias:

```mermaid
flowchart LR
Transaction -- Save Transaction with pending Status --> transactionDatabase[(Database)]
Transaction --Send transaction Created event--> Anti-Fraud
Anti-Fraud -- Send transaction Status Approved event--> Transaction
Anti-Fraud -- Send transaction Status Rejected event--> Transaction
Transaction -- Update transaction Status event--> transactionDatabase[(Database)]
```bash
npm install
# o
yarn install
```

# Tech Stack
2. Levanta infra con Docker Compose (Postgres, Zookeeper, Kafka):

<ol>
<li>Node. You can use any framework you want (i.e. Nestjs with an ORM like TypeOrm or Prisma) </li>
<li>Any database</li>
<li>Kafka</li>
</ol>
```bash
docker compose up -d
```

We do provide a `Dockerfile` to help you get started with a dev environment.
3. Ejecuta la aplicación en modo desarrollo:

You must have two resources:
```bash
npm run local
# o
yarn local
```

1. Resource to create a transaction that must containt:
### Levantar los dos servicios (transaction + anti-fraud)

```json
{
"accountExternalIdDebit": "Guid",
"accountExternalIdCredit": "Guid",
"tranferTypeId": 1,
"value": 120
}
Abre dos terminales separados y en cada uno ejecuta:

Terminal A (transaction):

```bash
cd transaction
npm install
npm run local
```

Terminal B (anti-fraud):

```bash
cd anti-fraud
npm install
npm run local
```

2. Resource to retrieve a transaction
Ambos servicios deben poder conectar al broker Kafka que levantaste con `docker compose up -d`.

## Cómo usar los servicios (endpoints)

Base URL: `http://localhost:3000`

1) Crear transacción — POST `/transaction`

- Descripción: crea una transacción y publica el evento para validación anti-fraud.
- Body (JSON):

```json
{
"transactionExternalId": "Guid",
"transactionType": {
"name": ""
},
"transactionStatus": {
"name": ""
},
"value": 120,
"createdAt": "Date"
"accountExternalIdDebit": "5429d629-c239-45fa-8235-1a386258c536",
"accountExternalIdCredit": "d6cd54da-8ce3-4f79-abda-bd5be9b19e68",
"tranferTypeId": 1,
"value": 120
}
```

## Optional
Ejemplos `curl`:

```bash
curl -i -X POST http://localhost:3000/transaction \
-H "Content-Type: application/json" \
-d '{
"accountExternalIdDebit": "5429d629-c239-45fa-8235-1a386258c536",
"accountExternalIdCredit": "d6cd54da-8ce3-4f79-abda-bd5be9b19e68",
"tranferTypeId": 1,
"value": 120
}'
```

2) Listar transacciones — GET `/transaction`

You can use any approach to store transaction data but you should consider that we may deal with high volume scenarios where we have a huge amount of writes and reads for the same data at the same time. How would you tackle this requirement?
- Descripción: devuelve todas las transacciones guardadas (útil para debug local).

You can use Graphql;
```bash
curl http://localhost:3000/transaction
```

# Send us your challenge
## Flujo Kafka (anti-fraud)

When you finish your challenge, after forking a repository, you **must** open a pull request to our repository. There are no limitations to the implementation, you can follow the programming paradigm, modularization, and style that you feel is the most appropriate solution.
- Al crear una transacción se publica un evento `transaction.created` con los datos básicos.
- Un consumidor (anti-fraud) valida la transacción y emite `transaction.validated` con `{ id, status }`.
- La app escucha `transaction.validated` y actualiza el `status` en la base de datos.

Regla implementada en el anti-fraud (ejemplo): `if value > 1000 => rejected`; else `approved`.

3. Consultar la última transacción:

```sql
select * from transaction where "accountExternalIdDebit" = {{ID}};
```

## DDL (esquema) para probar localmente

```sql

CREATE TABLE public.transaction
(
"accountExternalIdDebit" varchar NOT NULL,
"accountExternalIdCredit" varchar NOT NULL,
"tranferTypeId" integer NOT NULL,
value numeric NOT NULL,
id uuid DEFAULT uuid_generate_v4() NOT NULL
CONSTRAINT "PK_89eadb93a89810556e1cbcd6ab9"
PRIMARY KEY,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
status integer NOT NULL
);
```

If you have any questions, please let us know.
Si usas TypeORM, la entidad del proyecto crea automáticamente la tabla al iniciar (si `synchronize: true`). Usa este DDL solo si quieres preparar la tabla manualmente o probar fuera del ciclo de arranque de la app.
25 changes: 25 additions & 0 deletions anti-fraud/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};
4 changes: 4 additions & 0 deletions anti-fraud/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
8 changes: 8 additions & 0 deletions anti-fraud/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
}
}
77 changes: 77 additions & 0 deletions anti-fraud/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"name": "anti-fraud",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"local": "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": "^10.0.0",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^10.0.0",
"@nestjs/microservices": "^10.4.18",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/typeorm": "^11.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"kafkajs": "^2.2.4",
"pg": "^8.16.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"typeorm": "^0.3.24"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^8.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
17 changes: 17 additions & 0 deletions anti-fraud/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Controller, Logger } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';
import { AppService } from './app.service';
import { TransactionCreatedEvent } from './models/transaction-created.event';

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@MessagePattern('transaction.created')
async approvedTransaction(
@Payload() payload: TransactionCreatedEvent,
): Promise<void> {
Logger.debug(payload);
await this.appService.evaluateTransaction(payload);
}
}
29 changes: 29 additions & 0 deletions anti-fraud/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
imports: [
ConfigModule.forRoot(),
ClientsModule.register([
{
name: 'KAFKA',
transport: Transport.KAFKA,
options: {
client: {
clientId: 'anti-fraud',
brokers: [`${process.env.KAFKA_HOST}:9092`],
},
consumer: {
groupId: 'anti-fraud-consumer',
},
},
},
]),
],
controllers: [AppController],
providers: [AppService]
})
export class AppModule { }
Loading