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
4 changes: 4 additions & 0 deletions backend/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
87 changes: 87 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Anti-Fraud Transaction System

An application that processes financial transactions and validates them through an anti-fraud service using Kafka.

## Prerequisites

Before running the project, ensure you have the following installed:

- **Node.js** (v18 or higher)
- **Docker** and **Docker Compose** (for Kafka and Postgres)
- **NPM** (Node Package Manager)

## Setup & Installation

1. **Clone the repository** (if you haven't already).
2. **Navigate to the backend folder**:
```bash
cd backend
```
3. **Install dependencies**:
```bash
npm install
```

## Running the Application

Follow these steps in order:

### 1. Start Infrastructure
Start the Database (Postgres) and Message Broker (Kafka) using Docker:

```bash
docker-compose up -d
```
*Wait a minute for the containers to fully start.*

### 2. Start the Services
You need to run the **Transactions** API and **Anti-Fraud** service in separate terminals.

**Terminal 1: Transactions API**
```bash
npm run start:dev transactions
```

**Terminal 2: Anti-Fraud Service**
```bash
npm run start:dev anti-fraud
```

## How to Test

The system allows you to create transactions that are automatically Validated:
* **Approved**: Value is **1000 or less**.
* **Rejected**: Value is **greater than 1000**.

### JSON Request Body (for Postman)
Use this JSON payload for `POST http://localhost:3000/transactions`:

```json
{
"accountExternalIdDebit": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"accountExternalIdCredit": "82747195-257a-4d1a-be3b-7901748f0852",
"transferTypeId": 1,
"value": 500
}
```

### Using PowerShell
You can copy-paste this command to create a transaction:

```powershell
$body = @{
accountExternalIdDebit = "d290f1ee-6c54-4b01-90e6-d701748f0851"
accountExternalIdCredit = "82747195-257a-4d1a-be3b-7901748f0852"
transferTypeId = 1
value = 500
} | ConvertTo-Json

Invoke-RestMethod -Uri "http://localhost:3000/transactions" -Method Post -Body $body -ContentType "application/json"
```

### Check Status
To see the status of all transactions:

```powershell
Invoke-RestMethod -Uri "http://localhost:3000/transactions" -Method Get
```
20 changes: 20 additions & 0 deletions backend/apps/anti-fraud/src/anti-fraud.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Controller } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';
import { AntiFraudService } from './anti-fraud.service';

@Controller()
export class AntiFraudController {
constructor(private readonly antiFraudService: AntiFraudService) { }

@MessagePattern('transaction_created')
handleTransactionCreated(@Payload() message: any) {
console.log('AntiFraudController: Raw message type:', typeof message);
try {
console.log('AntiFraudController: Raw message JSON:', JSON.stringify(message));
} catch (e) {
console.log('AntiFraudController: Could not stringify message');
}
const data = message.value || message;
this.antiFraudService.validateTransaction(data);
}
}
26 changes: 26 additions & 0 deletions backend/apps/anti-fraud/src/anti-fraud.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { AntiFraudController } from './anti-fraud.controller';
import { AntiFraudService } from './anti-fraud.service';

@Module({
imports: [
ClientsModule.register([
{
name: 'KAFKA_SERVICE',
transport: Transport.KAFKA,
options: {
client: {
brokers: ['localhost:9092'],
},
consumer: {
groupId: 'anti-fraud-producer',
},
},
},
]),
],
controllers: [AntiFraudController],
providers: [AntiFraudService],
})
export class AntiFraudModule { }
21 changes: 21 additions & 0 deletions backend/apps/anti-fraud/src/anti-fraud.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Inject, Injectable } from '@nestjs/common';
import { ClientKafka } from '@nestjs/microservices';

@Injectable()
export class AntiFraudService {
constructor(
@Inject('KAFKA_SERVICE')
private readonly kafkaClient: ClientKafka,
) { }

validateTransaction(data: any) {
console.log('AntiFraudService: Validating transaction', data);
const status = data.amount > 1000 ? 'rejected' : 'approved';
console.log(`AntiFraudService: Emitting transaction_status_updated: ${status}`);

this.kafkaClient.emit('transaction_status_updated', {
transactionExternalId: data.transactionExternalId,
status,
}).subscribe();
}
}
22 changes: 22 additions & 0 deletions backend/apps/anti-fraud/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AntiFraudModule } from './anti-fraud.module';

async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AntiFraudModule,
{
transport: Transport.KAFKA,
options: {
client: {
brokers: ['localhost:9092'],
},
consumer: {
groupId: 'anti-fraud-consumer',
},
},
},
);
await app.listen();
}
bootstrap();
9 changes: 9 additions & 0 deletions backend/apps/anti-fraud/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": false,
"outDir": "../../dist/apps/anti-fraud"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}
23 changes: 23 additions & 0 deletions backend/apps/transactions/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { TransactionsModule } from './transactions.module';

async function bootstrap() {
const app = await NestFactory.create(TransactionsModule);

app.connectMicroservice<MicroserviceOptions>({
transport: Transport.KAFKA,
options: {
client: {
brokers: ['localhost:9092'],
},
consumer: {
groupId: 'transactions-consumer-server',
},
},
});

await app.startAllMicroservices();
await app.listen(process.env.port ?? 3000);
}
bootstrap();
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class CreateTransactionDto {
accountExternalIdDebit: string;
accountExternalIdCredit: string;
transferTypeId: number;
value: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';

export enum TransactionStatus {
PENDING = 'pending',
APPROVED = 'approved',
REJECTED = 'rejected',
}

@Entity()
export class Transaction {
@PrimaryGeneratedColumn('uuid')
transactionExternalId: string;

@Column('uuid')
accountExternalIdDebit: string;

@Column('uuid')
accountExternalIdCredit: string;

@Column('int')
transferTypeId: number;

@Column('decimal', { precision: 10, scale: 2 })
value: number;

@Column({
type: 'enum',
enum: TransactionStatus,
default: TransactionStatus.PENDING,
})
status: TransactionStatus;

@CreateDateColumn()
createdAt: Date;
}
36 changes: 36 additions & 0 deletions backend/apps/transactions/src/transactions.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Controller, Post, Body, Get } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';
import { TransactionsService } from './transactions.service';
import { CreateTransactionDto } from './transaction/dto/create-transaction.dto';
import { TransactionStatus } from './transaction/entities/transaction.entity';

@Controller('transactions')
export class TransactionsController {
constructor(private readonly transactionsService: TransactionsService) { }

@Post()
create(@Body() createTransactionDto: CreateTransactionDto) {
return this.transactionsService.create(createTransactionDto);
}

@MessagePattern('transaction_status_updated')
async handleTransactionStatusUpdated(@Payload() message: any) {
console.log('TransactionsController: Raw message', message);

// Kafka messages sometimes wrap the payload in a 'value' property or just return the object
const data = message.value || message;

if (!data.transactionExternalId) {
console.error('TransactionsController: Missing transactionExternalId', data);
return;
}

console.log(`TransactionsController: Updating ${data.transactionExternalId} to ${data.status}`);
await this.transactionsService.updateStatus(data.transactionExternalId, data.status);
}

@Get()
findAll() {
return this.transactionsService.findAll();
}
}
39 changes: 39 additions & 0 deletions backend/apps/transactions/src/transactions.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { TransactionsController } from './transactions.controller';
import { TransactionsService } from './transactions.service';
import { Transaction } from './transaction/entities/transaction.entity';

@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: '127.0.0.1',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'postgres', // Using the default database
entities: [Transaction],
synchronize: true, // Auto-create tables (dev only)
}),
TypeOrmModule.forFeature([Transaction]),
ClientsModule.register([
{
name: 'KAFKA_SERVICE',
transport: Transport.KAFKA,
options: {
client: {
brokers: ['localhost:9092'],
},
consumer: {
groupId: 'transactions-consumer',
},
},
},
]),
],
controllers: [TransactionsController],
providers: [TransactionsService],
})
export class TransactionsModule { }
42 changes: 42 additions & 0 deletions backend/apps/transactions/src/transactions.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ClientKafka } from '@nestjs/microservices';
import { Transaction, TransactionStatus } from './transaction/entities/transaction.entity';
import { CreateTransactionDto } from './transaction/dto/create-transaction.dto';

@Injectable()
export class TransactionsService {
constructor(
@InjectRepository(Transaction)
private transactionsRepository: Repository<Transaction>,
@Inject('KAFKA_SERVICE')
private readonly kafkaClient: ClientKafka,
) { }

async create(createTransactionDto: CreateTransactionDto): Promise<Transaction> {
const transaction = this.transactionsRepository.create({
...createTransactionDto,
status: TransactionStatus.PENDING,
});
const savedTransaction = await this.transactionsRepository.save(transaction);

// Emit event to anti-fraud service
const payload = {
transactionExternalId: savedTransaction.transactionExternalId,
amount: savedTransaction.value,
};
console.log(`TransactionsService: Emitting payload: ${JSON.stringify(payload)}`);
this.kafkaClient.emit('transaction_created', payload).subscribe();

return savedTransaction;
}

async updateStatus(transactionExternalId: string, status: TransactionStatus) {
await this.transactionsRepository.update(transactionExternalId, { status });
}

findAll() {
return this.transactionsRepository.find();
}
}
9 changes: 9 additions & 0 deletions backend/apps/transactions/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": false,
"outDir": "../../dist/apps/transactions"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}
Loading