diff --git a/backend/package-lock.json b/backend/package-lock.json index b53a385..0b0a5a2 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -26,6 +26,7 @@ "json2csv": "^6.0.0-alpha.2", "multer": "^2.0.2", "nestjs-i18n": "^10.5.1", + "papaparse": "^5.5.3", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "pdfkit": "^0.17.2", @@ -44,6 +45,7 @@ "@types/jest": "^29.5.2", "@types/json2csv": "^5.0.7", "@types/node": "^20.3.1", + "@types/papaparse": "^5.3.16", "@types/passport-jwt": "^4.0.1", "@types/pdfkit": "^0.17.3", "@types/pg": "^8.10.0", @@ -2313,6 +2315,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/papaparse": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.16.tgz", + "integrity": "sha512-T3VuKMC2H0lgsjI9buTB3uuKj3EMD2eap1MOuEQuBQ44EnDx/IkGhU6EwiTf9zG3za4SKlmwKAImdDKdNnCsXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/passport": { "version": "1.0.17", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", @@ -7264,6 +7276,12 @@ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", "license": "MIT" }, + "node_modules/papaparse": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index 094a4fd..ae3d2ca 100644 --- a/backend/package.json +++ b/backend/package.json @@ -37,6 +37,7 @@ "json2csv": "^6.0.0-alpha.2", "multer": "^2.0.2", "nestjs-i18n": "^10.5.1", + "papaparse": "^5.5.3", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "pdfkit": "^0.17.2", @@ -55,6 +56,7 @@ "@types/jest": "^29.5.2", "@types/json2csv": "^5.0.7", "@types/node": "^20.3.1", + "@types/papaparse": "^5.3.16", "@types/passport-jwt": "^4.0.1", "@types/pdfkit": "^0.17.3", "@types/pg": "^8.10.0", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 434c5e6..c1b8131 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -12,6 +12,7 @@ import { UsersModule } from './users/users.module'; import { User } from './users/entities/user.entity'; import { SearchModule } from './search/search.module'; import { AuthModule } from './auth/auth.module'; +import { ReportingModule } from './reporting/reporting.module'; @Module({ imports: [ @@ -46,6 +47,7 @@ import { AuthModule } from './auth/auth.module'; UsersModule, SearchModule, AuthModule, + ReportingModule, ], controllers: [AppController, NotificationsController], providers: [AppService, NotificationsService], diff --git a/backend/src/reporting/dto/report-query.dto.ts b/backend/src/reporting/dto/report-query.dto.ts new file mode 100644 index 0000000..3b43b3a --- /dev/null +++ b/backend/src/reporting/dto/report-query.dto.ts @@ -0,0 +1,7 @@ +import { IsIn, IsOptional } from 'class-validator'; + +export class ReportQueryDto { + @IsOptional() + @IsIn(['csv', 'pdf']) + format: 'csv' | 'pdf' = 'csv'; +} diff --git a/backend/src/reporting/reporting.controller.ts b/backend/src/reporting/reporting.controller.ts new file mode 100644 index 0000000..abfaea9 --- /dev/null +++ b/backend/src/reporting/reporting.controller.ts @@ -0,0 +1,32 @@ +import { Controller, Get, Param, Query, Res } from '@nestjs/common'; +import { Response } from 'express'; +import { ReportingService } from './reporting.service'; +import { ReportQueryDto } from './dto/report-query.dto'; + +@Controller('reports') +export class ReportingController { + constructor(private readonly reportingService: ReportingService) {} + + @Get(':jurisdiction') + async getReport( + @Param('jurisdiction') jurisdiction: string, + @Query() query: ReportQueryDto, + @Res() res: Response, + ) { + const { format } = query; + const stream = await this.reportingService.generateReport( + jurisdiction, + format, + ); + const filename = `report-${jurisdiction.toLowerCase()}-${Date.now()}.${format}`; + + res.setHeader('Content-Disposition', `attachment; filename=${filename}`); + if (format === 'pdf') { + res.setHeader('Content-Type', 'application/pdf'); + } else { + res.setHeader('Content-Type', 'text/csv'); + } + + stream.pipe(res); + } +} diff --git a/backend/src/reporting/reporting.module.ts b/backend/src/reporting/reporting.module.ts new file mode 100644 index 0000000..383a6b4 --- /dev/null +++ b/backend/src/reporting/reporting.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ReportingController } from './reporting.controller'; +import { ReportingService } from './reporting.service'; + +@Module({ + controllers: [ReportingController], + providers: [ReportingService], +}) +export class ReportingModule {} diff --git a/backend/src/reporting/reporting.service.ts b/backend/src/reporting/reporting.service.ts new file mode 100644 index 0000000..a56dd30 --- /dev/null +++ b/backend/src/reporting/reporting.service.ts @@ -0,0 +1,86 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import * as PDFDocument from 'pdfkit'; +import * as Papa from 'papaparse'; +import { Readable } from 'stream'; + +@Injectable() +export class ReportingService { + private async getReportData(jurisdiction: string): Promise { + const mockData = { + USA: [ + { id: 'TX-123', amount: 5000, currency: 'USD', status: 'approved' }, + { id: 'NY-456', amount: 12000, currency: 'USD', status: 'pending' }, + ], + EU: [ + { id: 'DE-789', amount: 8000, currency: 'EUR', status: 'approved' }, + { id: 'FR-101', amount: 7500, currency: 'EUR', status: 'declined' }, + ], + }; + + const data = mockData[jurisdiction.toUpperCase()]; + if (!data) { + throw new NotFoundException( + `No data found for jurisdiction: ${jurisdiction}`, + ); + } + return data; + } + + async generateReport( + jurisdiction: string, + format: 'csv' | 'pdf', + ): Promise { + const data = await this.getReportData(jurisdiction); + + if (format === 'csv') { + return this.generateCsv(data); + } else { + return this.generatePdf(data, jurisdiction); + } + } + + private generateCsv(data: any[]): Readable { + const csvString = Papa.unparse(data); + const stream = new Readable(); + stream.push(csvString); + stream.push(null); + return stream; + } + + private generatePdf(data: any[], jurisdiction: string): Readable { + const doc = new PDFDocument({ margin: 50 }); + + doc.fontSize(20).text(`Regulatory Report: ${jurisdiction.toUpperCase()}`, { + align: 'center', + }); + doc.moveDown(); + + const tableTop = 150; + const itemX = 50; + const amountX = 250; + const currencyX = 350; + const statusX = 450; + + doc + .fontSize(12) + .text('Transaction ID', itemX, tableTop) + .text('Amount', amountX, tableTop) + .text('Currency', currencyX, tableTop) + .text('Status', statusX, tableTop); + + let i = 0; + for (const item of data) { + const y = tableTop + 25 + i * 25; + doc + .fontSize(10) + .text(item.id, itemX, y) + .text(item.amount.toString(), amountX, y) + .text(item.currency, currencyX, y) + .text(item.status, statusX, y); + i++; + } + + doc.end(); + return doc; + } +}