Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e036dc6
add: update .gitignore to include WebStorm IDE files
joshnavdev Jun 7, 2025
a74e8cc
feat: add transaction service
joshnavdev Jun 7, 2025
1a02972
feat: init anti-fraud microservice
joshnavdev Jun 7, 2025
93abdb3
feat: add config module with kafka configuration
joshnavdev Jun 7, 2025
76feb0f
feat: implement anti-fraud service with transaction validation and ev…
joshnavdev Jun 7, 2025
ad23585
refactor: update transaction-service to api-gateway
joshnavdev Jun 7, 2025
9eeb0ef
feat: update api-gateway to make kafka event pattern
joshnavdev Jun 8, 2025
881e5e9
refactor: improve code readability and formatting in anti-fraud service
joshnavdev Jun 8, 2025
3776e88
feat: implement transaction service with create, approve, and reject …
joshnavdev Jun 8, 2025
e34f0e1
feat: add Docker support with multi-stage build and .dockerignore for…
joshnavdev Jun 8, 2025
7f333e8
feat: add Docker support with multi-stage build and .dockerignore for…
joshnavdev Jun 8, 2025
7b34b20
feat: add Docker support with multi-stage build and .dockerignore for…
joshnavdev Jun 8, 2025
47e85fe
feat: add database connection and migration scripts for transaction s…
joshnavdev Jun 8, 2025
a64d11e
feat: enhance docker-compose with health checks and add db-migration …
joshnavdev Jun 8, 2025
f3de569
feat: add seeding script and update migration Dockerfile to include s…
joshnavdev Jun 8, 2025
b5ba33c
feat: init graphql-gateway
joshnavdev Jun 8, 2025
2acce68
feat: implement transaction service with GraphQL support and configur…
joshnavdev Jun 8, 2025
b22add3
feat: implement transaction service with GraphQL support and configur…
joshnavdev Jun 8, 2025
53d909f
feat: update tsconfig to exclude database directory from build
joshnavdev Jun 8, 2025
6dbb27d
feat: enable introspection for GraphQL in app module
joshnavdev Jun 8, 2025
b8f2784
feat: update docker-compose to set PORT and adjust Kafka client IDs f…
joshnavdev Jun 8, 2025
d8e7b93
feat: add unit tests for AntiFraud service
joshnavdev Jun 8, 2025
dad3aa0
feat: add unit tests for transaction service and related configurations
joshnavdev Jun 8, 2025
1eedf35
feat: update README to transaction-service
joshnavdev Jun 9, 2025
cc6ed06
feat: update README with formatting improvements and additional details
joshnavdev Jun 9, 2025
6223a7a
feat: update README to include Anti-Fraud Service details and validat…
joshnavdev Jun 9, 2025
3df67f4
fix: remove transaction validation emission from transaction event im…
joshnavdev Jun 9, 2025
1edc33d
feat: update README to include GraphQL API Gateway details and usage …
joshnavdev Jun 9, 2025
a89a272
fix: remove transaction validation emission from transaction event im…
joshnavdev Jun 9, 2025
f87f9c7
feat: update README to include details about the REST API Gateway and…
joshnavdev Jun 9, 2025
3a8d920
feat: implement Redis caching for transaction service
joshnavdev Jun 9, 2025
e6ac051
feat: add transaction service and repository implementation with tests
joshnavdev Jun 9, 2025
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

# WebStorm IDE files
.idea
2 changes: 2 additions & 0 deletions anti-fraud-service/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
.dist/
5 changes: 5 additions & 0 deletions anti-fraud-service/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120
}
30 changes: 30 additions & 0 deletions anti-fraud-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
FROM node:22-alpine as development

WORKDIR /app

COPY package.json yarn.lock ./

RUN yarn install --frozen-lockfile

COPY . .

RUN yarn build

FROM node:22-alpine AS production

ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}

WORKDIR /app

COPY package.json yarn.lock ./

RUN yarn install --frozen-lockfile --production

COPY . .

COPY --from=development /app/dist ./dist

EXPOSE 3000

CMD ["yarn", "run", "start:prod"]
62 changes: 62 additions & 0 deletions anti-fraud-service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Anti-Fraude Service

## Description

The Anti-Fraud Service is responsible for validating financial transactions based on predefined business rules. It is
part of a distributed, event-driven architecture and communicates asynchronously with other services using Kafka.

## Features

- Listen for `transaction_created` events to validate transactions.
- Applies anti-fraud validates logic (e.g., rejecting transaction over a certain threshold).
- Emits either `approve_transaction` or `reject_transaction` events based on validation results.

## Tech Stack

- Node.js - using the NestJS framework
- Kafka - for event-driven communication
- Jest - for unit testing

## Validation Rules

- Transactions with a `value > 1000` are automatically **rejected**.
- All other transactions are **approved**.

## Kafka Topics

- **Produces:**
- `approve_transaction` - emitted to update the status of a transaction to `APPROVED`.
- `reject_transaction` - emitted to update the status of a transaction to `REJECTED`.
- **Consumes**:
- `transaction_created` - Received when a new transaction is created, to validate it.

## Environment Variables

| Variable | Description |
|----------------------------|------------------------------|
| `KAFKA_BROKER_0` | Kafka broker address |
| `KAFKA_CLIENT_ID_CONSUMER` | Kafka client ID for consumer |
| `KAFKA_CLIENT_ID_PRODUCER` | Kafka client ID for producer |
| `KAFKA_GROUP_ID` | Kafka consumer group ID |

## Running the Service

To run the service in **development** mode:

```bash
yarn start:dev
```

To run the service in **production** mode:

```bash
yarn start:prod
```

## Running Tests

To execute the test suite:

```bash
yarn test
```
35 changes: 35 additions & 0 deletions anti-fraud-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': 'off',
'@typescript-eslint/no-unsafe-argument': 'warn',
'@typescript-eslint/unbound-method': 'off',
},
},
);
8 changes: 8 additions & 0 deletions anti-fraud-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
}
}
84 changes: 84 additions & 0 deletions anti-fraud-service/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"name": "anti-fraud-service",
"version": "0.0.1",
"description": "",
"author": "Joshua Navarro <joshua.navarro35@gmail.com>",
"engines": {
"node": ">=22.0.0"
},
"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/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/microservices": "^11.1.3",
"@nestjs/platform-express": "^11.0.1",
"joi": "^17.13.3",
"kafkajs": "^2.2.4",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"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": "^16.0.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",
"coveragePathIgnorePatterns": [
".*\\.module\\.ts$",
".*main\\.ts$"
]
}
}
33 changes: 33 additions & 0 deletions anti-fraud-service/src/anti-fraud/anti-fraud.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import AntiFraudService from './anti-fraud.service';
import TransactionDto from './dtos/transaction.dto';
import AntiFraudController from './anti-fraud.controller';
import { Test } from '@nestjs/testing';

describe('AntiFraudController', () => {
let controller: AntiFraudController;
let mockAntiFraudService: jest.Mocked<AntiFraudService>;

beforeEach(async () => {
mockAntiFraudService = {
validateTransaction: jest.fn(),
emitTransactionStatusUpdate: jest.fn(),
} as unknown as jest.Mocked<AntiFraudService>;
const module = await Test.createTestingModule({
controllers: [AntiFraudController],
providers: [{ provide: AntiFraudService, useValue: mockAntiFraudService }],
}).compile();
controller = module.get<AntiFraudController>(AntiFraudController);
});

describe('transactionCreated', () => {
it('should validate transaction and emit status update', () => {
const transaction: TransactionDto = new TransactionDto('trx-id-01', 1200);

mockAntiFraudService.validateTransaction.mockReturnValue(true);

controller.transactionCreated(transaction);
expect(mockAntiFraudService.validateTransaction).toHaveBeenCalledWith(transaction);
expect(mockAntiFraudService.emitTransactionStatusUpdate).toHaveBeenCalledWith(transaction.id, true);
});
});
});
20 changes: 20 additions & 0 deletions anti-fraud-service/src/anti-fraud/anti-fraud.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Controller, Inject, Logger } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';
import TransactionDto from './dtos/transaction.dto';
import AntiFraudService from './anti-fraud.service';

@Controller()
export default class AntiFraudController {
private readonly logger = new Logger(AntiFraudController.name);

constructor(@Inject() public readonly antiFraudService: AntiFraudService) {}

@MessagePattern('transaction_created')
transactionCreated(@Payload() transaction: TransactionDto): void {
this.logger.log('Transaction created event received');
const isFraudulent = this.antiFraudService.validateTransaction(transaction);

this.logger.log('Transaction validation completed');
this.antiFraudService.emitTransactionStatusUpdate(transaction.id, isFraudulent);
}
}
33 changes: 33 additions & 0 deletions anti-fraud-service/src/anti-fraud/anti-fraud.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Module } from '@nestjs/common';
import AntiFraudController from './anti-fraud.controller';
import AntiFraudService from './anti-fraud.service';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { ANTI_FRAUD_EVENT_PRODUCER } from './tokens';
import { ConfigService } from '@nestjs/config';
import { KafkaConfig } from '../config/kafka.config';

@Module({
imports: [
ClientsModule.registerAsync([
{
name: ANTI_FRAUD_EVENT_PRODUCER,
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
const kafkaConfig = configService.get<KafkaConfig>('kafka');
return {
transport: Transport.KAFKA,
options: {
client: {
clientId: kafkaConfig?.client.producerId,
brokers: kafkaConfig?.brokers as string[],
},
},
};
},
},
]),
],
controllers: [AntiFraudController],
providers: [AntiFraudService],
})
export class AntiFraudModule {}
54 changes: 54 additions & 0 deletions anti-fraud-service/src/anti-fraud/anti-fraud.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import AntiFraudService from './anti-fraud.service';
import { ClientKafka } from '@nestjs/microservices';
import { Test, TestingModule } from '@nestjs/testing';
import { ANTI_FRAUD_EVENT_PRODUCER } from './tokens';
import TransactionDto from './dtos/transaction.dto';
import { APPROVE_TRANSACTION_TOPIC, REJECT_TRANSACTION_TOPIC, TRANSACTION_AMOUNT_THRESHOLD } from './constants';

describe('AntiFraudService', () => {
let service: AntiFraudService;
let mockClientProducer: ClientKafka;

beforeEach(async () => {
mockClientProducer = {
emit: jest.fn(),
} as unknown as ClientKafka;
const module: TestingModule = await Test.createTestingModule({
providers: [
AntiFraudService,
{
provide: ANTI_FRAUD_EVENT_PRODUCER,
useValue: mockClientProducer,
},
],
}).compile();

service = module.get<AntiFraudService>(AntiFraudService);
});

describe('validateTransaction', () => {
it('should return true for transactions above the threshold', () => {
const dto: TransactionDto = { id: 'test', amount: TRANSACTION_AMOUNT_THRESHOLD + 1 };
expect(service.validateTransaction(dto)).toBe(true);
});

it('should return false for transactions below the threshold', () => {
const dto: TransactionDto = { id: 'test', amount: TRANSACTION_AMOUNT_THRESHOLD - 1 };
expect(service.validateTransaction(dto)).toBe(false);
});
});

describe('emitTransactionStatusUpdate', () => {
it('should emit reject transaction event for fraudulent transactions', () => {
const trxId = 'fraudulent-transaction';
service.emitTransactionStatusUpdate(trxId, true);
expect(mockClientProducer.emit).toHaveBeenCalledWith(REJECT_TRANSACTION_TOPIC, { transactionId: trxId });
});

it('should emit approve transaction event for non-fraudulent transactions', () => {
const trxId = 'valid-transaction';
service.emitTransactionStatusUpdate(trxId, false);
expect(mockClientProducer.emit).toHaveBeenCalledWith(APPROVE_TRANSACTION_TOPIC, { transactionId: trxId });
});
});
});
Loading