From 82a0ff99074af688c89504b3f623b04fe4316d10 Mon Sep 17 00:00:00 2001 From: TuanLamNguyen Date: Fri, 27 Feb 2026 10:36:21 +0100 Subject: [PATCH 1/5] feat: create two endpoints for pdf generation for transactions in blockchain. --- .../payment/services/swiss-qr.service.ts | 105 ++++++++++++++++++ .../controllers/realunit.controller.ts | 82 ++++++++++++++ .../realunit/dto/realunit-pdf.dto.ts | 33 ++++++ .../supporting/realunit/realunit.service.ts | 56 +++++++++- 4 files changed, 275 insertions(+), 1 deletion(-) diff --git a/src/subdomains/supporting/payment/services/swiss-qr.service.ts b/src/subdomains/supporting/payment/services/swiss-qr.service.ts index c48fa674b5..c66d39f0af 100644 --- a/src/subdomains/supporting/payment/services/swiss-qr.service.ts +++ b/src/subdomains/supporting/payment/services/swiss-qr.service.ts @@ -3,10 +3,13 @@ import { I18nService } from 'nestjs-i18n'; import PDFDocument from 'pdfkit'; import * as QRCode from 'qrcode'; import { Config } from 'src/config/config'; +import { Asset } from 'src/shared/models/asset/asset.entity'; import { AssetService } from 'src/shared/models/asset/asset.service'; import { LogoSize, PdfBrand, PdfUtil } from 'src/shared/utils/pdf.util'; +import { AmountType, Util } from 'src/shared/utils/util'; import { BankInfoDto } from 'src/subdomains/core/buy-crypto/routes/buy/dto/buy-payment-info.dto'; import { UserData } from 'src/subdomains/generic/user/models/user-data/user-data.entity'; +import { HistoryEventDto } from 'src/subdomains/supporting/realunit/dto/realunit.dto'; import { PDFColumn, PDFRow, SwissQRBill, Table } from 'swissqrbill/pdf'; import { SwissQRCode } from 'swissqrbill/svg'; import { Creditor, Debtor, Data as QrBillData } from 'swissqrbill/types'; @@ -147,6 +150,108 @@ export class SwissQRService { return this.generateMultiPdfInvoice(tableDataWithType, language, billData, brand); } + // --- History Receipt Methods --- + + async createHistoryTxReceipt( + historyEvent: HistoryEventDto, + userData: UserData, + asset: Asset, + fiatPrice: number, + currency: 'CHF' | 'EUR', + isIncoming: boolean, + brand: PdfBrand = PdfBrand.REALUNIT, + ): Promise { + const debtor = this.getDebtor(userData); + const language = this.getLanguage(userData); + const tokenAmount = Number(historyEvent.transfer.value); + const fiatAmount = Util.roundReadable(tokenAmount * fiatPrice, AmountType.FIAT); + + const tableData: SwissQRBillTableData = { + title: this.translate('invoice.receipt_title', language.toLowerCase(), { + invoiceId: this.shortenTxHash(historyEvent.txHash), + }), + quantity: tokenAmount, + description: { + assetDescription: asset.description ?? asset.name, + assetName: asset.name, + assetBlockchain: asset.blockchain, + }, + fiatAmount, + date: historyEvent.timestamp, + }; + + const billData: QrBillData = { + creditor: this.getDefaultCreditor(brand), + debtor, + currency, + }; + + const transactionType = isIncoming ? TransactionType.BUY : TransactionType.SELL; + return this.generatePdfInvoice( + tableData, + language, + billData, + undefined, + transactionType, + brand, + userData.completeName, + ); + } + + async createMultiHistoryTxReceipt( + receipts: Array<{ + historyEvent: HistoryEventDto; + fiatPrice: number; + isIncoming: boolean; + }>, + userData: UserData, + asset: Asset, + currency: 'CHF' | 'EUR', + brand: PdfBrand = PdfBrand.REALUNIT, + ): Promise { + if (receipts.length === 0) throw new Error('At least one transaction is required'); + + const debtor = this.getDebtor(userData); + const language = this.getLanguage(userData); + + const tableDataWithType: { data: SwissQRBillTableData; type: TransactionType }[] = []; + + for (const { historyEvent, fiatPrice, isIncoming } of receipts) { + const tokenAmount = Number(historyEvent.transfer.value); + const fiatAmount = Util.roundReadable(tokenAmount * fiatPrice, AmountType.FIAT); + + const tableData: SwissQRBillTableData = { + title: this.translate('invoice.receipt_title', language.toLowerCase(), { + invoiceId: this.shortenTxHash(historyEvent.txHash), + }), + quantity: tokenAmount, + description: { + assetDescription: asset.description ?? asset.name, + assetName: asset.name, + assetBlockchain: asset.blockchain, + }, + fiatAmount, + date: historyEvent.timestamp, + }; + + const transactionType = isIncoming ? TransactionType.BUY : TransactionType.SELL; + tableDataWithType.push({ data: tableData, type: transactionType }); + } + + const billData: QrBillData = { + creditor: this.getDefaultCreditor(brand), + debtor, + currency, + }; + + return this.generateMultiPdfInvoice(tableDataWithType, language, billData, brand); + } + + private shortenTxHash(txHash: string): string { + if (txHash.length <= 16) return txHash; + return `${txHash.slice(0, 8)}...${txHash.slice(-6)}`; + } + private async generatePdfInvoice( tableData: SwissQRBillTableData, language: string, diff --git a/src/subdomains/supporting/realunit/controllers/realunit.controller.ts b/src/subdomains/supporting/realunit/controllers/realunit.controller.ts index aa35f4dfe9..22e6ba6e2c 100644 --- a/src/subdomains/supporting/realunit/controllers/realunit.controller.ts +++ b/src/subdomains/supporting/realunit/controllers/realunit.controller.ts @@ -40,17 +40,22 @@ import { RoleGuard } from 'src/shared/auth/role.guard'; import { UserActiveGuard } from 'src/shared/auth/user-active.guard'; import { UserRole } from 'src/shared/auth/user-role.enum'; import { PdfBrand } from 'src/shared/utils/pdf.util'; +import { Util } from 'src/shared/utils/util'; import { PdfDto } from 'src/subdomains/core/buy-crypto/routes/buy/dto/pdf.dto'; import { UserService } from 'src/subdomains/generic/user/models/user/user.service'; import { BalancePdfService } from '../../balance/services/balance-pdf.service'; import { TxStatementType } from '../../payment/dto/transaction-helper/tx-statement-details.dto'; import { SwissQRService } from '../../payment/services/swiss-qr.service'; import { TransactionHelper } from '../../payment/services/transaction-helper'; +import { PriceCurrency, PricingService } from '../../pricing/services/pricing.service'; import { RealUnitAdminQueryDto, RealUnitQuoteDto, RealUnitTransactionDto } from '../dto/realunit-admin.dto'; import { RealUnitBalancePdfDto, + RealUnitHistoryMultiReceiptDto, + RealUnitHistorySingleReceiptDto, RealUnitMultiReceiptPdfDto, RealUnitSingleReceiptPdfDto, + ReceiptCurrency, } from '../dto/realunit-pdf.dto'; import { RealUnitEmailRegistrationDto, @@ -87,6 +92,7 @@ export class RealUnitController { private readonly userService: UserService, private readonly transactionHelper: TransactionHelper, private readonly swissQrService: SwissQRService, + private readonly pricingService: PricingService, ) {} @Get('account/:address') @@ -228,6 +234,82 @@ export class RealUnitController { return { pdfData: await this.swissQrService.createMultiTxStatement(txStatementDetails, PdfBrand.REALUNIT) }; } + // --- History Receipt Endpoints --- + + @Post('transactions/receipt/history/single') + @ApiBearerAuth() + @UseGuards(AuthGuard(), RoleGuard(UserRole.USER), UserActiveGuard()) + @ApiOperation({ + summary: 'Generate receipt from blockchain history', + description: 'Generates a PDF receipt for any RealUnit transaction found in blockchain history', + }) + @ApiOkResponse({ type: PdfDto, description: 'Receipt PDF (base64 encoded)' }) + @ApiBadRequestResponse({ description: 'Transaction not found or not a transfer' }) + async generateHistoryReceipt( + @GetJwt() jwt: JwtPayload, + @Body() dto: RealUnitHistorySingleReceiptDto, + ): Promise { + const user = await this.userService.getUser(jwt.user, { userData: true }); + const currency = dto.currency ?? ReceiptCurrency.CHF; + const historyEvent = await this.realunitService.getHistoryEventByTxHash(jwt.address, dto.txHash); + const realuAsset = await this.realunitService.getRealuAsset(); + const price = await this.pricingService.getPriceAt(realuAsset, PriceCurrency[currency], historyEvent.timestamp); + const isIncoming = Util.equalsIgnoreCase(historyEvent.transfer.to, jwt.address); + + const pdfData = await this.swissQrService.createHistoryTxReceipt( + historyEvent, + user.userData, + realuAsset, + price.price, + currency, + isIncoming, + PdfBrand.REALUNIT, + ); + + return { pdfData }; + } + + @Post('transactions/receipt/history/multi') + @ApiBearerAuth() + @UseGuards(AuthGuard(), RoleGuard(UserRole.USER), UserActiveGuard()) + @ApiOperation({ + summary: 'Generate multi-receipt from blockchain history', + description: 'Generates a single PDF receipt for multiple RealUnit transactions found in blockchain history', + }) + @ApiOkResponse({ type: PdfDto, description: 'Receipt PDF (base64 encoded)' }) + @ApiBadRequestResponse({ description: 'Transaction not found or not a transfer' }) + async generateHistoryMultiReceipt( + @GetJwt() jwt: JwtPayload, + @Body() dto: RealUnitHistoryMultiReceiptDto, + ): Promise { + const user = await this.userService.getUser(jwt.user, { userData: true }); + const currency = dto.currency ?? ReceiptCurrency.CHF; + const historyEvents = await this.realunitService.getHistoryEventsByTxHashes(jwt.address, dto.txHashes); + const realuAsset = await this.realunitService.getRealuAsset(); + + const receipts = await Promise.all( + historyEvents.map(async (event) => { + const price = await this.pricingService.getPriceAt(realuAsset, PriceCurrency[currency], event.timestamp); + const isIncoming = Util.equalsIgnoreCase(event.transfer.to, jwt.address); + return { + historyEvent: event, + fiatPrice: price.price, + isIncoming, + }; + }), + ); + + const pdfData = await this.swissQrService.createMultiHistoryTxReceipt( + receipts, + user.userData, + realuAsset, + currency, + PdfBrand.REALUNIT, + ); + + return { pdfData }; + } + // --- Brokerbot Endpoints --- @Get('brokerbot/info') diff --git a/src/subdomains/supporting/realunit/dto/realunit-pdf.dto.ts b/src/subdomains/supporting/realunit/dto/realunit-pdf.dto.ts index eb6ea65f47..f33162a11e 100644 --- a/src/subdomains/supporting/realunit/dto/realunit-pdf.dto.ts +++ b/src/subdomains/supporting/realunit/dto/realunit-pdf.dto.ts @@ -9,6 +9,7 @@ import { IsNotEmpty, IsNumber, IsOptional, + IsString, } from 'class-validator'; import { PdfLanguage } from 'src/subdomains/supporting/balance/dto/input/get-balance-pdf.dto'; import { PriceCurrency } from 'src/subdomains/supporting/pricing/services/pricing.service'; @@ -49,3 +50,35 @@ export class RealUnitMultiReceiptPdfDto { @ArrayMinSize(1) transactionIds: number[]; } + +// --- History Receipt DTOs --- + +export enum ReceiptCurrency { + CHF = 'CHF', + EUR = 'EUR', +} + +export class RealUnitHistorySingleReceiptDto { + @ApiProperty({ description: 'Transaction hash from blockchain history' }) + @IsNotEmpty() + @IsString() + txHash: string; + + @ApiPropertyOptional({ description: 'Currency for fiat values on receipt', enum: ReceiptCurrency, default: ReceiptCurrency.CHF }) + @IsOptional() + @IsEnum(ReceiptCurrency) + currency?: ReceiptCurrency = ReceiptCurrency.CHF; +} + +export class RealUnitHistoryMultiReceiptDto { + @ApiProperty({ type: [String], description: 'Array of transaction hashes from blockchain history' }) + @IsArray() + @IsString({ each: true }) + @ArrayMinSize(1) + txHashes: string[]; + + @ApiPropertyOptional({ description: 'Currency for fiat values on receipt', enum: ReceiptCurrency, default: ReceiptCurrency.CHF }) + @IsOptional() + @IsEnum(ReceiptCurrency) + currency?: ReceiptCurrency = ReceiptCurrency.CHF; +} diff --git a/src/subdomains/supporting/realunit/realunit.service.ts b/src/subdomains/supporting/realunit/realunit.service.ts index 5ceeb7c73e..39bc614cde 100644 --- a/src/subdomains/supporting/realunit/realunit.service.ts +++ b/src/subdomains/supporting/realunit/realunit.service.ts @@ -53,6 +53,7 @@ import { PriceCurrency, PriceValidity, PricingService } from '../pricing/service import { AccountHistoryClientResponse, AccountSummaryClientResponse, + HistoryEventType, HoldersClientResponse, TokenInfoClientResponse, } from './dto/client.dto'; @@ -74,6 +75,7 @@ import { AccountHistoryDto, AccountSummaryDto, HistoricalPriceDto, + HistoryEventDto, HoldersDto, RealUnitBuyDto, RealUnitPaymentInfoDto, @@ -147,7 +149,59 @@ export class RealUnitService { return RealUnitDtoMapper.toAccountHistoryDto(clientResponse); } - private async getRealuAsset(): Promise { + async getHistoryEventByTxHash(address: string, txHash: string): Promise { + const normalizedTxHash = txHash.toLowerCase(); + let cursor: string | undefined; + + while (true) { + const history = await this.getAccountHistory(address, 100, cursor); + + const event = history.history.find( + (e) => e.txHash.toLowerCase() === normalizedTxHash && e.eventType === HistoryEventType.TRANSFER, + ); + + if (event) return event; + + if (!history.pageInfo.hasNextPage) break; + cursor = history.pageInfo.endCursor; + } + + throw new NotFoundException('Transaction not found in account history'); + } + + async getHistoryEventsByTxHashes(address: string, txHashes: string[]): Promise { + const normalizedHashes = new Set(txHashes.map((h) => h.toLowerCase())); + const foundEvents: HistoryEventDto[] = []; + let cursor: string | undefined; + + while (foundEvents.length < txHashes.length) { + const history = await this.getAccountHistory(address, 100, cursor); + + for (const event of history.history) { + if ( + normalizedHashes.has(event.txHash.toLowerCase()) && + event.eventType === HistoryEventType.TRANSFER && + !foundEvents.some((e) => e.txHash.toLowerCase() === event.txHash.toLowerCase()) + ) { + foundEvents.push(event); + } + } + + if (!history.pageInfo.hasNextPage) break; + cursor = history.pageInfo.endCursor; + } + + // Verify all requested transactions were found + const foundHashes = new Set(foundEvents.map((e) => e.txHash.toLowerCase())); + const missingHashes = txHashes.filter((h) => !foundHashes.has(h.toLowerCase())); + if (missingHashes.length > 0) { + throw new NotFoundException(`Transactions not found: ${missingHashes.join(', ')}`); + } + + return foundEvents; + } + + async getRealuAsset(): Promise { return this.assetService.getAssetByQuery({ name: this.tokenName, blockchain: this.tokenBlockchain, From dd5bd0f4c3ce0c108a31d061c89bc1cabeea9f71 Mon Sep 17 00:00:00 2001 From: TuanLamNguyen Date: Fri, 27 Feb 2026 10:47:41 +0100 Subject: [PATCH 2/5] fix: price calculation. --- .../supporting/realunit/controllers/realunit.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/subdomains/supporting/realunit/controllers/realunit.controller.ts b/src/subdomains/supporting/realunit/controllers/realunit.controller.ts index 22e6ba6e2c..9714fc2467 100644 --- a/src/subdomains/supporting/realunit/controllers/realunit.controller.ts +++ b/src/subdomains/supporting/realunit/controllers/realunit.controller.ts @@ -260,7 +260,7 @@ export class RealUnitController { historyEvent, user.userData, realuAsset, - price.price, + price.convert(1), currency, isIncoming, PdfBrand.REALUNIT, @@ -293,7 +293,7 @@ export class RealUnitController { const isIncoming = Util.equalsIgnoreCase(event.transfer.to, jwt.address); return { historyEvent: event, - fiatPrice: price.price, + fiatPrice: price.convert(1), isIncoming, }; }), From 4f5f054aed0bec9b085eb644d655f021d893cd50 Mon Sep 17 00:00:00 2001 From: TuanLamNguyen Date: Fri, 27 Feb 2026 11:04:01 +0100 Subject: [PATCH 3/5] feat: remove existent logic for receipt. --- .../payment/services/swiss-qr.service.ts | 36 +------- .../controllers/realunit.controller.ts | 91 ++----------------- .../realunit/dto/realunit-pdf.dto.ts | 42 ++++----- 3 files changed, 27 insertions(+), 142 deletions(-) diff --git a/src/subdomains/supporting/payment/services/swiss-qr.service.ts b/src/subdomains/supporting/payment/services/swiss-qr.service.ts index c66d39f0af..ce15181c65 100644 --- a/src/subdomains/supporting/payment/services/swiss-qr.service.ts +++ b/src/subdomains/supporting/payment/services/swiss-qr.service.ts @@ -120,39 +120,7 @@ export class SwissQRService { ); } - async createMultiTxStatement(details: TxStatementDetails[], brand: PdfBrand = PdfBrand.DFX): Promise { - if (details.length === 0) throw new Error('At least one transaction is required'); - - const firstDetail = details[0]; - const debtor = this.getDebtor(firstDetail.transaction.userData); - if (!debtor) throw new Error('Debtor is required'); - - const validatedCurrency = this.validateCurrency(firstDetail.currency); - const language = this.getLanguage(firstDetail.transaction.userData); - - const tableDataWithType: { data: SwissQRBillTableData; type: TransactionType }[] = []; - for (const detail of details) { - const tableData = await this.getTableData( - detail.statementType, - detail.transactionType, - detail.transaction, - validatedCurrency, - ); - tableDataWithType.push({ data: tableData, type: detail.transactionType }); - } - - const billData: QrBillData = { - creditor: this.getDefaultCreditor(brand), - debtor, - currency: validatedCurrency, - }; - - return this.generateMultiPdfInvoice(tableDataWithType, language, billData, brand); - } - - // --- History Receipt Methods --- - - async createHistoryTxReceipt( + async createTxFromBlockchainReceipt( historyEvent: HistoryEventDto, userData: UserData, asset: Asset, @@ -198,7 +166,7 @@ export class SwissQRService { ); } - async createMultiHistoryTxReceipt( + async createTxFromBlockchainMultiReceipt( receipts: Array<{ historyEvent: HistoryEventDto; fiatPrice: number; diff --git a/src/subdomains/supporting/realunit/controllers/realunit.controller.ts b/src/subdomains/supporting/realunit/controllers/realunit.controller.ts index 9714fc2467..e83c4b8212 100644 --- a/src/subdomains/supporting/realunit/controllers/realunit.controller.ts +++ b/src/subdomains/supporting/realunit/controllers/realunit.controller.ts @@ -1,16 +1,4 @@ -import { - BadRequestException, - Body, - Controller, - Get, - HttpStatus, - Param, - Post, - Put, - Query, - Res, - UseGuards, -} from '@nestjs/common'; +import { Body, Controller, Get, HttpStatus, Param, Post, Put, Query, Res, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiAcceptedResponse, @@ -44,15 +32,11 @@ import { Util } from 'src/shared/utils/util'; import { PdfDto } from 'src/subdomains/core/buy-crypto/routes/buy/dto/pdf.dto'; import { UserService } from 'src/subdomains/generic/user/models/user/user.service'; import { BalancePdfService } from '../../balance/services/balance-pdf.service'; -import { TxStatementType } from '../../payment/dto/transaction-helper/tx-statement-details.dto'; import { SwissQRService } from '../../payment/services/swiss-qr.service'; -import { TransactionHelper } from '../../payment/services/transaction-helper'; import { PriceCurrency, PricingService } from '../../pricing/services/pricing.service'; import { RealUnitAdminQueryDto, RealUnitQuoteDto, RealUnitTransactionDto } from '../dto/realunit-admin.dto'; import { RealUnitBalancePdfDto, - RealUnitHistoryMultiReceiptDto, - RealUnitHistorySingleReceiptDto, RealUnitMultiReceiptPdfDto, RealUnitSingleReceiptPdfDto, ReceiptCurrency, @@ -90,7 +74,6 @@ export class RealUnitController { private readonly realunitService: RealUnitService, private readonly balancePdfService: BalancePdfService, private readonly userService: UserService, - private readonly transactionHelper: TransactionHelper, private readonly swissQrService: SwissQRService, private readonly pricingService: PricingService, ) {} @@ -162,7 +145,7 @@ export class RealUnitController { return this.realunitService.getRealUnitInfo(); } - // --- Balance PDF Endpoint --- + // --- PDF Endpoints --- @Post('balance/pdf') @ApiBearerAuth() @@ -183,72 +166,16 @@ export class RealUnitController { return { pdfData }; } - // --- Receipt PDF Endpoint --- - @Post('transactions/receipt/single') @ApiBearerAuth() @UseGuards(AuthGuard(), RoleGuard(UserRole.USER), UserActiveGuard()) @ApiOperation({ - description: 'Generates a PDF receipt for a completed RealUnit transaction', - }) - @ApiParam({ name: 'id', description: 'Transaction ID' }) - @ApiOkResponse({ type: PdfDto, description: 'Receipt PDF (base64 encoded)' }) - @ApiBadRequestResponse({ description: 'Transaction not found or not a RealUnit transaction' }) - async generateReceipt(@GetJwt() jwt: JwtPayload, @Body() dto: RealUnitSingleReceiptPdfDto): Promise { - const user = await this.userService.getUser(jwt.user, { userData: true }); - - const txStatementDetails = await this.transactionHelper.getTxStatementDetails( - user.userData.id, - dto.transactionId, - TxStatementType.RECEIPT, - ); - - if (!Config.invoice.currencies.includes(txStatementDetails.currency)) { - throw new BadRequestException('PDF receipt is only available for CHF and EUR transactions'); - } - - return { pdfData: await this.swissQrService.createTxStatement(txStatementDetails, PdfBrand.REALUNIT) }; - } - - @Post('transactions/receipt/multi') - @ApiBearerAuth() - @UseGuards(AuthGuard(), RoleGuard(UserRole.USER), UserActiveGuard()) - @ApiOperation({ - description: 'Generates a single PDF receipt for multiple completed RealUnit transactions', - }) - @ApiOkResponse({ type: PdfDto, description: 'Receipt PDF (base64 encoded)' }) - @ApiBadRequestResponse({ description: 'Transaction not found, currency mismatch, or not a RealUnit transaction' }) - async generateMultiReceipt(@GetJwt() jwt: JwtPayload, @Body() dto: RealUnitMultiReceiptPdfDto): Promise { - const user = await this.userService.getUser(jwt.user, { userData: true }); - - const txStatementDetails = await this.transactionHelper.getTxStatementDetailsMulti( - user.userData.id, - dto.transactionIds, - TxStatementType.RECEIPT, - ); - - if (txStatementDetails.length > 0 && !Config.invoice.currencies.includes(txStatementDetails[0].currency)) { - throw new BadRequestException('PDF receipt is only available for CHF and EUR transactions'); - } - - return { pdfData: await this.swissQrService.createMultiTxStatement(txStatementDetails, PdfBrand.REALUNIT) }; - } - - // --- History Receipt Endpoints --- - - @Post('transactions/receipt/history/single') - @ApiBearerAuth() - @UseGuards(AuthGuard(), RoleGuard(UserRole.USER), UserActiveGuard()) - @ApiOperation({ - summary: 'Generate receipt from blockchain history', + summary: 'Generate receipt from blockchain transaction', description: 'Generates a PDF receipt for any RealUnit transaction found in blockchain history', }) @ApiOkResponse({ type: PdfDto, description: 'Receipt PDF (base64 encoded)' }) @ApiBadRequestResponse({ description: 'Transaction not found or not a transfer' }) - async generateHistoryReceipt( - @GetJwt() jwt: JwtPayload, - @Body() dto: RealUnitHistorySingleReceiptDto, - ): Promise { + async generateHistoryReceipt(@GetJwt() jwt: JwtPayload, @Body() dto: RealUnitSingleReceiptPdfDto): Promise { const user = await this.userService.getUser(jwt.user, { userData: true }); const currency = dto.currency ?? ReceiptCurrency.CHF; const historyEvent = await this.realunitService.getHistoryEventByTxHash(jwt.address, dto.txHash); @@ -256,7 +183,7 @@ export class RealUnitController { const price = await this.pricingService.getPriceAt(realuAsset, PriceCurrency[currency], historyEvent.timestamp); const isIncoming = Util.equalsIgnoreCase(historyEvent.transfer.to, jwt.address); - const pdfData = await this.swissQrService.createHistoryTxReceipt( + const pdfData = await this.swissQrService.createTxFromBlockchainReceipt( historyEvent, user.userData, realuAsset, @@ -269,18 +196,18 @@ export class RealUnitController { return { pdfData }; } - @Post('transactions/receipt/history/multi') + @Post('transactions/receipt/multi') @ApiBearerAuth() @UseGuards(AuthGuard(), RoleGuard(UserRole.USER), UserActiveGuard()) @ApiOperation({ - summary: 'Generate multi-receipt from blockchain history', + summary: 'Generate multi-receipt from blockchain transactions', description: 'Generates a single PDF receipt for multiple RealUnit transactions found in blockchain history', }) @ApiOkResponse({ type: PdfDto, description: 'Receipt PDF (base64 encoded)' }) @ApiBadRequestResponse({ description: 'Transaction not found or not a transfer' }) async generateHistoryMultiReceipt( @GetJwt() jwt: JwtPayload, - @Body() dto: RealUnitHistoryMultiReceiptDto, + @Body() dto: RealUnitMultiReceiptPdfDto, ): Promise { const user = await this.userService.getUser(jwt.user, { userData: true }); const currency = dto.currency ?? ReceiptCurrency.CHF; @@ -299,7 +226,7 @@ export class RealUnitController { }), ); - const pdfData = await this.swissQrService.createMultiHistoryTxReceipt( + const pdfData = await this.swissQrService.createTxFromBlockchainMultiReceipt( receipts, user.userData, realuAsset, diff --git a/src/subdomains/supporting/realunit/dto/realunit-pdf.dto.ts b/src/subdomains/supporting/realunit/dto/realunit-pdf.dto.ts index f33162a11e..37e231e1e4 100644 --- a/src/subdomains/supporting/realunit/dto/realunit-pdf.dto.ts +++ b/src/subdomains/supporting/realunit/dto/realunit-pdf.dto.ts @@ -7,13 +7,17 @@ import { IsEnum, IsEthereumAddress, IsNotEmpty, - IsNumber, IsOptional, IsString, } from 'class-validator'; import { PdfLanguage } from 'src/subdomains/supporting/balance/dto/input/get-balance-pdf.dto'; import { PriceCurrency } from 'src/subdomains/supporting/pricing/services/pricing.service'; +export enum ReceiptCurrency { + CHF = 'CHF', + EUR = 'EUR', +} + export class RealUnitBalancePdfDto { @ApiProperty({ description: 'Blockchain address (EVM)' }) @IsNotEmpty() @@ -37,47 +41,33 @@ export class RealUnitBalancePdfDto { } export class RealUnitSingleReceiptPdfDto { - @ApiProperty({ type: Number, description: 'Transaction ID' }) - @IsNumber() - @Type(() => Number) - transactionId: number; -} - -export class RealUnitMultiReceiptPdfDto { - @ApiProperty({ type: [Number], description: 'Array of transaction IDs to include in the receipt' }) - @IsArray() - @IsNumber({}, { each: true }) - @ArrayMinSize(1) - transactionIds: number[]; -} - -// --- History Receipt DTOs --- - -export enum ReceiptCurrency { - CHF = 'CHF', - EUR = 'EUR', -} - -export class RealUnitHistorySingleReceiptDto { @ApiProperty({ description: 'Transaction hash from blockchain history' }) @IsNotEmpty() @IsString() txHash: string; - @ApiPropertyOptional({ description: 'Currency for fiat values on receipt', enum: ReceiptCurrency, default: ReceiptCurrency.CHF }) + @ApiPropertyOptional({ + description: 'Currency for fiat values on receipt', + enum: ReceiptCurrency, + default: ReceiptCurrency.CHF, + }) @IsOptional() @IsEnum(ReceiptCurrency) currency?: ReceiptCurrency = ReceiptCurrency.CHF; } -export class RealUnitHistoryMultiReceiptDto { +export class RealUnitMultiReceiptPdfDto { @ApiProperty({ type: [String], description: 'Array of transaction hashes from blockchain history' }) @IsArray() @IsString({ each: true }) @ArrayMinSize(1) txHashes: string[]; - @ApiPropertyOptional({ description: 'Currency for fiat values on receipt', enum: ReceiptCurrency, default: ReceiptCurrency.CHF }) + @ApiPropertyOptional({ + description: 'Currency for fiat values on receipt', + enum: ReceiptCurrency, + default: ReceiptCurrency.CHF, + }) @IsOptional() @IsEnum(ReceiptCurrency) currency?: ReceiptCurrency = ReceiptCurrency.CHF; From 892404ab49a7a79c2f1b6fa3f6dffc3307a8517c Mon Sep 17 00:00:00 2001 From: TuanLamNguyen Date: Fri, 27 Feb 2026 11:49:35 +0100 Subject: [PATCH 4/5] feat: remove terms and conditions from realu receipt. --- .../payment/services/swiss-qr.service.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/subdomains/supporting/payment/services/swiss-qr.service.ts b/src/subdomains/supporting/payment/services/swiss-qr.service.ts index ce15181c65..c46b7bd483 100644 --- a/src/subdomains/supporting/payment/services/swiss-qr.service.ts +++ b/src/subdomains/supporting/payment/services/swiss-qr.service.ts @@ -163,6 +163,7 @@ export class SwissQRService { transactionType, brand, userData.completeName, + true, ); } @@ -212,12 +213,12 @@ export class SwissQRService { currency, }; - return this.generateMultiPdfInvoice(tableDataWithType, language, billData, brand); + return this.generateMultiPdfInvoice(tableDataWithType, language, billData, brand, true); } private shortenTxHash(txHash: string): string { if (txHash.length <= 16) return txHash; - return `${txHash.slice(0, 8)}...${txHash.slice(-6)}`; + return `${txHash.slice(0, 8)}...${txHash.slice(-8)}`; } private async generatePdfInvoice( @@ -228,6 +229,7 @@ export class SwissQRService { transactionType: TransactionType, brand: PdfBrand = PdfBrand.DFX, debtorName?: string, + skipTermsAndConditions = false, ): Promise { const { pdf, promise } = this.createPdfWithBase64Promise(); @@ -364,8 +366,6 @@ export class SwissQRService { }, ]; - const termsAndConditions = this.getTermsAndConditions(language); - if (bankInfo) { rows.push({ columns: [ @@ -378,7 +378,10 @@ export class SwissQRService { ], }); } - rows.push({ columns: [termsAndConditions] }); + + if (!skipTermsAndConditions) { + rows.push({ columns: [this.getTermsAndConditions(language)] }); + } const table = new Table({ rows, width: mm2pt(170) }); table.attachTo(pdf); @@ -442,6 +445,7 @@ export class SwissQRService { language: string, billData: QrBillData, brand: PdfBrand = PdfBrand.DFX, + skipTermsAndConditions = false, ): Promise { const { pdf, promise } = this.createPdfWithBase64Promise(); @@ -614,7 +618,9 @@ export class SwissQRService { padding: 5, }); - rows.push({ columns: [this.getTermsAndConditions(language)] }); + if (!skipTermsAndConditions) { + rows.push({ columns: [this.getTermsAndConditions(language)] }); + } const table = new Table({ rows, width: mm2pt(170) }); table.attachTo(pdf); From febfeea06a2ce82929b57362afd4bf8e2622b68b Mon Sep 17 00:00:00 2001 From: TuanLamNguyen Date: Fri, 27 Feb 2026 12:00:56 +0100 Subject: [PATCH 5/5] chore: cleaning. --- src/subdomains/supporting/realunit/realunit.service.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/subdomains/supporting/realunit/realunit.service.ts b/src/subdomains/supporting/realunit/realunit.service.ts index 39bc614cde..e8e8b2d2e7 100644 --- a/src/subdomains/supporting/realunit/realunit.service.ts +++ b/src/subdomains/supporting/realunit/realunit.service.ts @@ -191,13 +191,6 @@ export class RealUnitService { cursor = history.pageInfo.endCursor; } - // Verify all requested transactions were found - const foundHashes = new Set(foundEvents.map((e) => e.txHash.toLowerCase())); - const missingHashes = txHashes.filter((h) => !foundHashes.has(h.toLowerCase())); - if (missingHashes.length > 0) { - throw new NotFoundException(`Transactions not found: ${missingHashes.join(', ')}`); - } - return foundEvents; }