Skip to content

Commit b13aead

Browse files
Merge pull request #324 from Ibinola/feat/regulatory-reporting-module
feat(reporting): implement regulatory reporting module
2 parents 69b426a + 60d8d8d commit b13aead

File tree

7 files changed

+156
-0
lines changed

7 files changed

+156
-0
lines changed

backend/package-lock.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"json2csv": "^6.0.0-alpha.2",
3838
"multer": "^2.0.2",
3939
"nestjs-i18n": "^10.5.1",
40+
"papaparse": "^5.5.3",
4041
"passport": "^0.7.0",
4142
"passport-jwt": "^4.0.1",
4243
"pdfkit": "^0.17.2",
@@ -55,6 +56,7 @@
5556
"@types/jest": "^29.5.2",
5657
"@types/json2csv": "^5.0.7",
5758
"@types/node": "^20.3.1",
59+
"@types/papaparse": "^5.3.16",
5860
"@types/passport-jwt": "^4.0.1",
5961
"@types/pdfkit": "^0.17.3",
6062
"@types/pg": "^8.10.0",

backend/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { UsersModule } from './users/users.module';
1212
import { User } from './users/entities/user.entity';
1313
import { SearchModule } from './search/search.module';
1414
import { AuthModule } from './auth/auth.module';
15+
import { ReportingModule } from './reporting/reporting.module';
1516

1617
@Module({
1718
imports: [
@@ -46,6 +47,7 @@ import { AuthModule } from './auth/auth.module';
4647
UsersModule,
4748
SearchModule,
4849
AuthModule,
50+
ReportingModule,
4951
],
5052
controllers: [AppController, NotificationsController],
5153
providers: [AppService, NotificationsService],
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { IsIn, IsOptional } from 'class-validator';
2+
3+
export class ReportQueryDto {
4+
@IsOptional()
5+
@IsIn(['csv', 'pdf'])
6+
format: 'csv' | 'pdf' = 'csv';
7+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Controller, Get, Param, Query, Res } from '@nestjs/common';
2+
import { Response } from 'express';
3+
import { ReportingService } from './reporting.service';
4+
import { ReportQueryDto } from './dto/report-query.dto';
5+
6+
@Controller('reports')
7+
export class ReportingController {
8+
constructor(private readonly reportingService: ReportingService) {}
9+
10+
@Get(':jurisdiction')
11+
async getReport(
12+
@Param('jurisdiction') jurisdiction: string,
13+
@Query() query: ReportQueryDto,
14+
@Res() res: Response,
15+
) {
16+
const { format } = query;
17+
const stream = await this.reportingService.generateReport(
18+
jurisdiction,
19+
format,
20+
);
21+
const filename = `report-${jurisdiction.toLowerCase()}-${Date.now()}.${format}`;
22+
23+
res.setHeader('Content-Disposition', `attachment; filename=${filename}`);
24+
if (format === 'pdf') {
25+
res.setHeader('Content-Type', 'application/pdf');
26+
} else {
27+
res.setHeader('Content-Type', 'text/csv');
28+
}
29+
30+
stream.pipe(res);
31+
}
32+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Module } from '@nestjs/common';
2+
import { ReportingController } from './reporting.controller';
3+
import { ReportingService } from './reporting.service';
4+
5+
@Module({
6+
controllers: [ReportingController],
7+
providers: [ReportingService],
8+
})
9+
export class ReportingModule {}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { Injectable, NotFoundException } from '@nestjs/common';
2+
import * as PDFDocument from 'pdfkit';
3+
import * as Papa from 'papaparse';
4+
import { Readable } from 'stream';
5+
6+
@Injectable()
7+
export class ReportingService {
8+
private async getReportData(jurisdiction: string): Promise<any[]> {
9+
const mockData = {
10+
USA: [
11+
{ id: 'TX-123', amount: 5000, currency: 'USD', status: 'approved' },
12+
{ id: 'NY-456', amount: 12000, currency: 'USD', status: 'pending' },
13+
],
14+
EU: [
15+
{ id: 'DE-789', amount: 8000, currency: 'EUR', status: 'approved' },
16+
{ id: 'FR-101', amount: 7500, currency: 'EUR', status: 'declined' },
17+
],
18+
};
19+
20+
const data = mockData[jurisdiction.toUpperCase()];
21+
if (!data) {
22+
throw new NotFoundException(
23+
`No data found for jurisdiction: ${jurisdiction}`,
24+
);
25+
}
26+
return data;
27+
}
28+
29+
async generateReport(
30+
jurisdiction: string,
31+
format: 'csv' | 'pdf',
32+
): Promise<Readable> {
33+
const data = await this.getReportData(jurisdiction);
34+
35+
if (format === 'csv') {
36+
return this.generateCsv(data);
37+
} else {
38+
return this.generatePdf(data, jurisdiction);
39+
}
40+
}
41+
42+
private generateCsv(data: any[]): Readable {
43+
const csvString = Papa.unparse(data);
44+
const stream = new Readable();
45+
stream.push(csvString);
46+
stream.push(null);
47+
return stream;
48+
}
49+
50+
private generatePdf(data: any[], jurisdiction: string): Readable {
51+
const doc = new PDFDocument({ margin: 50 });
52+
53+
doc.fontSize(20).text(`Regulatory Report: ${jurisdiction.toUpperCase()}`, {
54+
align: 'center',
55+
});
56+
doc.moveDown();
57+
58+
const tableTop = 150;
59+
const itemX = 50;
60+
const amountX = 250;
61+
const currencyX = 350;
62+
const statusX = 450;
63+
64+
doc
65+
.fontSize(12)
66+
.text('Transaction ID', itemX, tableTop)
67+
.text('Amount', amountX, tableTop)
68+
.text('Currency', currencyX, tableTop)
69+
.text('Status', statusX, tableTop);
70+
71+
let i = 0;
72+
for (const item of data) {
73+
const y = tableTop + 25 + i * 25;
74+
doc
75+
.fontSize(10)
76+
.text(item.id, itemX, y)
77+
.text(item.amount.toString(), amountX, y)
78+
.text(item.currency, currencyX, y)
79+
.text(item.status, statusX, y);
80+
i++;
81+
}
82+
83+
doc.end();
84+
return doc;
85+
}
86+
}

0 commit comments

Comments
 (0)