Skip to content

Commit 0fa121f

Browse files
authored
Feat: add IP log PDF export and enrich compliance user details (#3501)
Add IP log PDF endpoint, extend transaction info with input/EUR amounts, add user ref field, and increase IP log default limit to 100.
1 parent 9b94820 commit 0fa121f

4 files changed

Lines changed: 130 additions & 1 deletion

File tree

src/shared/models/ip-log/ip-log.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class IpLogService {
5353
return this.ipLogRepo.save(ipLog);
5454
}
5555

56-
async getByUserDataId(userDataId: number, limit = 50): Promise<IpLog[]> {
56+
async getByUserDataId(userDataId: number, limit = 100): Promise<IpLog[]> {
5757
return this.ipLogRepo.find({
5858
where: { userData: { id: userDataId } },
5959
order: { created: 'DESC' },

src/subdomains/generic/support/dto/user-data-support.dto.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export class BankTxSupportInfo {
3434
export class UserSupportInfo {
3535
id: number;
3636
address: string;
37+
ref?: string;
3738
role: string;
3839
status: string;
3940
created: Date;
@@ -44,7 +45,10 @@ export class TransactionSupportInfo {
4445
uid: string;
4546
type?: string;
4647
sourceType: string;
48+
inputAmount?: number;
49+
inputAsset?: string;
4750
amountInChf?: number;
51+
amountInEur?: number;
4852
amlCheck?: string;
4953
chargebackDate?: Date;
5054
amlReason?: string;

src/subdomains/generic/support/support.controller.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ export class SupportController {
7272
return this.supportService.getRecommendationGraph(+id);
7373
}
7474

75+
@Get(':id/ip-log-pdf')
76+
@ApiBearerAuth()
77+
@ApiExcludeEndpoint()
78+
@UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard())
79+
async getIpLogPdf(@Param('id') id: string): Promise<{ pdfData: string }> {
80+
const pdfData = await this.supportService.generateIpLogPdf(+id);
81+
return { pdfData };
82+
}
83+
7584
@Get(':id')
7685
@ApiBearerAuth()
7786
@ApiExcludeEndpoint()

src/subdomains/generic/support/support.service.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { BadRequestException, Inject, Injectable, NotFoundException, forwardRef } from '@nestjs/common';
2+
import PDFDocument from 'pdfkit';
3+
import { PdfUtil } from 'src/shared/utils/pdf.util';
24
import { isIP } from 'class-validator';
35
import * as IbanTools from 'ibantools';
46
import { Config } from 'src/config/config';
@@ -109,6 +111,116 @@ export class SupportService {
109111
private readonly supportIssueService: SupportIssueService,
110112
) {}
111113

114+
async generateIpLogPdf(userDataId: number): Promise<string> {
115+
const ipLogs = await this.ipLogService.getByUserDataId(userDataId);
116+
117+
return new Promise<string>((resolve, reject) => {
118+
try {
119+
const pdf = new PDFDocument({ size: 'A4', margin: 50 });
120+
const chunks: Buffer[] = [];
121+
122+
pdf.on('data', (chunk) => chunks.push(chunk));
123+
pdf.on('end', () => resolve(Buffer.concat(chunks).toString('base64')));
124+
125+
PdfUtil.drawLogo(pdf);
126+
127+
// Header
128+
const marginX = 50;
129+
pdf.moveDown(2);
130+
pdf.fontSize(18).font('Helvetica-Bold').fillColor('#072440');
131+
pdf.text('IP Log Report', marginX);
132+
pdf.moveDown(0.5);
133+
pdf.fontSize(10).font('Helvetica').fillColor('#333333');
134+
pdf.text(`User Data ID: ${userDataId}`, marginX);
135+
pdf.text(`Date: ${new Date().toISOString().split('T')[0]}`, marginX);
136+
pdf.text(`Total Entries: ${ipLogs.length}`, marginX);
137+
pdf.moveDown(1);
138+
139+
// Table
140+
this.drawIpLogTable(pdf, ipLogs);
141+
142+
// Footer
143+
pdf.moveDown(2);
144+
pdf.fontSize(8).font('Helvetica').fillColor('#999999');
145+
pdf.text(`Generated by DFX - ${new Date().toISOString()}`, marginX);
146+
147+
pdf.end();
148+
} catch (e) {
149+
reject(e);
150+
}
151+
});
152+
}
153+
154+
private drawIpLogTable(pdf: InstanceType<typeof PDFDocument>, ipLogs: IpLog[]): void {
155+
const marginX = 50;
156+
const { width } = pdf.page;
157+
const tableWidth = width - marginX * 2;
158+
159+
const cols = [
160+
{ header: 'Date', width: tableWidth * 0.2 },
161+
{ header: 'IP', width: tableWidth * 0.2 },
162+
{ header: 'Country', width: tableWidth * 0.12 },
163+
{ header: 'Endpoint', width: tableWidth * 0.36 },
164+
{ header: 'Status', width: tableWidth * 0.12 },
165+
];
166+
167+
let y = pdf.y;
168+
169+
// Headers
170+
pdf.fontSize(10).font('Helvetica-Bold').fillColor('#072440');
171+
let x = marginX;
172+
for (const col of cols) {
173+
pdf.text(col.header, x, y, { width: col.width - 5 });
174+
x += col.width;
175+
}
176+
177+
y += 18;
178+
pdf
179+
.moveTo(marginX, y)
180+
.lineTo(width - marginX, y)
181+
.stroke('#CCCCCC');
182+
y += 8;
183+
184+
// Rows
185+
pdf.fontSize(9).font('Helvetica').fillColor('#333333');
186+
187+
if (ipLogs.length === 0) {
188+
pdf.text('No IP logs found', marginX, y);
189+
} else {
190+
for (const log of ipLogs) {
191+
if (y > pdf.page.height - 80) {
192+
pdf.addPage();
193+
y = 50;
194+
}
195+
196+
x = marginX;
197+
const date = log.created ? new Date(log.created).toISOString().replace('T', ' ').substring(0, 19) : '-';
198+
const endpoint = log.url?.replace('/v1/', '') ?? '-';
199+
200+
pdf.fillColor('#333333');
201+
pdf.text(date, x, y, { width: cols[0].width - 5 });
202+
x += cols[0].width;
203+
pdf.text(log.ip ?? '-', x, y, { width: cols[1].width - 5 });
204+
x += cols[1].width;
205+
pdf.text(log.country ?? '-', x, y, { width: cols[2].width - 5 });
206+
x += cols[2].width;
207+
pdf.text(endpoint, x, y, { width: cols[3].width - 5 });
208+
x += cols[3].width;
209+
210+
pdf.fillColor(log.result ? '#28a745' : '#dc3545');
211+
pdf.text(log.result ? 'Pass' : 'Fail', x, y, { width: cols[4].width - 5 });
212+
213+
y += 20;
214+
}
215+
}
216+
217+
pdf
218+
.moveTo(marginX, y)
219+
.lineTo(width - marginX, y)
220+
.stroke('#CCCCCC');
221+
pdf.y = y + 10;
222+
}
223+
112224
async getUserDataDetails(id: number): Promise<UserDataSupportInfoDetails> {
113225
const userData = await this.userDataService.getUserData(id, { wallet: true, bankDatas: true });
114226
if (!userData) throw new NotFoundException(`User not found`);
@@ -328,7 +440,10 @@ export class SupportService {
328440
uid: tx.uid,
329441
type: tx.type,
330442
sourceType: tx.sourceType,
443+
inputAmount: tx.buyCrypto?.inputAmount ?? tx.buyFiat?.inputAmount,
444+
inputAsset: tx.buyCrypto?.inputAsset ?? tx.buyFiat?.inputAsset,
331445
amountInChf: tx.amountInChf,
446+
amountInEur: tx.buyCrypto?.amountInEur ?? tx.buyFiat?.amountInEur,
332447
amlCheck: tx.amlCheck,
333448
chargebackDate:
334449
tx.buyCrypto?.chargebackDate ??
@@ -344,6 +459,7 @@ export class SupportService {
344459
return {
345460
id: user.id,
346461
address: user.address,
462+
ref: user.ref,
347463
role: user.role,
348464
status: user.status,
349465
created: user.created,

0 commit comments

Comments
 (0)