From 9e3048d43a426120972866102bc4eb7c11bfa332 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:56:24 +0200 Subject: [PATCH] feat(realunit): overhaul transaction receipt PDF layout and i18n Add language override parameter to receipt DTOs so the app can request receipts in the user's current locale instead of the profile language. Extend the RealUnit receipt with the fields requested by RealUnit Schweiz AG: ISIN-qualified description ("Namenaktie / Registerwertrecht"), unit price per share, fee disclosure ("spesenfrei"), execution date+time, buyer name, wallet address, full tx hash, and payment method. Add RealUnit-specific i18n keys in all four languages (DE/EN/FR/IT). Refactor generatePdfInvoice and generateMultiPdfInvoice to conditionally render the enhanced layout when brand is REALUNIT. --- src/shared/i18n/de/invoice.json | 16 +- src/shared/i18n/en/invoice.json | 16 +- src/shared/i18n/fr/invoice.json | 16 +- src/shared/i18n/it/invoice.json | 16 +- .../payment/services/swiss-qr.service.ts | 475 +++++++++++------- .../controllers/realunit.controller.ts | 4 + .../realunit/dto/realunit-pdf.dto.ts | 10 + 7 files changed, 359 insertions(+), 194 deletions(-) diff --git a/src/shared/i18n/de/invoice.json b/src/shared/i18n/de/invoice.json index e3feb9335b..67c069c79f 100644 --- a/src/shared/i18n/de/invoice.json +++ b/src/shared/i18n/de/invoice.json @@ -40,5 +40,19 @@ "payment_info_iban": "IBAN", "payment_info_bic": "BIC", "payment_info_recipient": "Empfänger", - "payment_info_reference": "Verwendungszweck" + "payment_info_reference": "Verwendungszweck", + "realunit_receipt": { + "buy_description": "RealUnit Namenaktie (Aktientoken REALU), ISIN CH1137233305 — Registerwertrecht via Ethereum-Blockchain", + "sell_description": "RealUnit Namenaktie (Aktientoken REALU), ISIN CH1137233305 — Registerwertrecht via Ethereum-Blockchain", + "unit_price_label": "Kurs pro Aktie", + "fees_label": "Spesen", + "fees_free": "spesenfrei", + "details_title": "Transaktionsdetails", + "buyer_label": "Käufer / Inhaber", + "wallet_label": "Wallet-Adresse", + "tx_hash_label": "Transaktions-Hash", + "payment_method_label": "Zahlungsart", + "payment_method_blockchain": "Blockchain-Transfer", + "receipt_total_label": "Total" + } } diff --git a/src/shared/i18n/en/invoice.json b/src/shared/i18n/en/invoice.json index af2aa02654..db73ae7f61 100644 --- a/src/shared/i18n/en/invoice.json +++ b/src/shared/i18n/en/invoice.json @@ -40,5 +40,19 @@ "payment_info_iban": "IBAN", "payment_info_bic": "BIC", "payment_info_recipient": "Recipient", - "payment_info_reference": "Remittance Info" + "payment_info_reference": "Remittance Info", + "realunit_receipt": { + "buy_description": "RealUnit registered share (share token REALU), ISIN CH1137233305 — registered value right via Ethereum Blockchain", + "sell_description": "RealUnit registered share (share token REALU), ISIN CH1137233305 — registered value right via Ethereum Blockchain", + "unit_price_label": "Price per share", + "fees_label": "Fees", + "fees_free": "fee-free", + "details_title": "Transaction Details", + "buyer_label": "Buyer / Holder", + "wallet_label": "Wallet address", + "tx_hash_label": "Transaction hash", + "payment_method_label": "Payment method", + "payment_method_blockchain": "Blockchain transfer", + "receipt_total_label": "Receipt Total" + } } diff --git a/src/shared/i18n/fr/invoice.json b/src/shared/i18n/fr/invoice.json index ad2bff83c2..2ce060ade1 100644 --- a/src/shared/i18n/fr/invoice.json +++ b/src/shared/i18n/fr/invoice.json @@ -40,5 +40,19 @@ "payment_info_iban": "IBAN", "payment_info_bic": "BIC", "payment_info_recipient": "Bénéficiaire", - "payment_info_reference": "Motif de paiement" + "payment_info_reference": "Motif de paiement", + "realunit_receipt": { + "buy_description": "Action nominative RealUnit (jeton d'action REALU), ISIN CH1137233305 — droit-valeur inscrit via Ethereum Blockchain", + "sell_description": "Action nominative RealUnit (jeton d'action REALU), ISIN CH1137233305 — droit-valeur inscrit via Ethereum Blockchain", + "unit_price_label": "Cours par action", + "fees_label": "Frais", + "fees_free": "sans frais", + "details_title": "Détails de la transaction", + "buyer_label": "Acheteur / Détenteur", + "wallet_label": "Adresse du portefeuille", + "tx_hash_label": "Hash de transaction", + "payment_method_label": "Moyen de paiement", + "payment_method_blockchain": "Transfert Blockchain", + "receipt_total_label": "Total du reçu" + } } diff --git a/src/shared/i18n/it/invoice.json b/src/shared/i18n/it/invoice.json index c8af6aff29..8cd5814108 100644 --- a/src/shared/i18n/it/invoice.json +++ b/src/shared/i18n/it/invoice.json @@ -40,5 +40,19 @@ "payment_info_iban": "IBAN", "payment_info_bic": "BIC", "payment_info_recipient": "Beneficiario", - "payment_info_reference": "Causale" + "payment_info_reference": "Causale", + "realunit_receipt": { + "buy_description": "Azione nominativa RealUnit (token azionario REALU), ISIN CH1137233305 — diritto patrimoniale registrato via Ethereum Blockchain", + "sell_description": "Azione nominativa RealUnit (token azionario REALU), ISIN CH1137233305 — diritto patrimoniale registrato via Ethereum Blockchain", + "unit_price_label": "Prezzo per azione", + "fees_label": "Spese", + "fees_free": "senza spese", + "details_title": "Dettagli della transazione", + "buyer_label": "Acquirente / Titolare", + "wallet_label": "Indirizzo del portafoglio", + "tx_hash_label": "Hash della transazione", + "payment_method_label": "Metodo di pagamento", + "payment_method_blockchain": "Trasferimento Blockchain", + "receipt_total_label": "Totale della ricevuta" + } } diff --git a/src/subdomains/supporting/payment/services/swiss-qr.service.ts b/src/subdomains/supporting/payment/services/swiss-qr.service.ts index 30174e197c..955e1f3f6f 100644 --- a/src/subdomains/supporting/payment/services/swiss-qr.service.ts +++ b/src/subdomains/supporting/payment/services/swiss-qr.service.ts @@ -37,6 +37,10 @@ interface SwissQRBillTableData { description: any; fiatAmount: number; date: Date; + unitPrice?: number; + txHash?: string; + walletAddress?: string; + buyerName?: string; } @Injectable() @@ -128,9 +132,11 @@ export class SwissQRService { currency: 'CHF' | 'EUR', isIncoming: boolean, brand: PdfBrand = PdfBrand.REALUNIT, + languageOverride?: string, + walletAddress?: string, ): Promise { const debtor = this.getDebtor(userData); - const language = this.getLanguage(userData); + const language = languageOverride ?? this.getLanguage(userData); const tokenAmount = Number(historyEvent.transfer.value); const fiatAmount = Util.roundReadable(tokenAmount * fiatPrice, AmountType.FIAT); @@ -146,6 +152,10 @@ export class SwissQRService { }, fiatAmount, date: historyEvent.timestamp, + unitPrice: fiatPrice, + txHash: historyEvent.txHash, + walletAddress, + buyerName: userData.completeName, }; const billData: QrBillData = { @@ -177,11 +187,13 @@ export class SwissQRService { asset: Asset, currency: 'CHF' | 'EUR', brand: PdfBrand = PdfBrand.REALUNIT, + languageOverride?: string, + walletAddress?: string, ): 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 language = languageOverride ?? this.getLanguage(userData); const tableDataWithType: { data: SwissQRBillTableData; type: TransactionType }[] = []; @@ -201,6 +213,8 @@ export class SwissQRService { }, fiatAmount, date: historyEvent.timestamp, + unitPrice: fiatPrice, + txHash: historyEvent.txHash, }; const transactionType = isIncoming ? TransactionType.BUY : TransactionType.SELL; @@ -213,7 +227,15 @@ export class SwissQRService { currency, }; - return this.generateMultiPdfInvoice(tableDataWithType, language, billData, brand, true); + return this.generateMultiPdfInvoice( + tableDataWithType, + language, + billData, + brand, + true, + walletAddress, + userData.completeName, + ); } private shortenTxHash(txHash: string): string { @@ -232,37 +254,82 @@ export class SwissQRService { skipTermsAndConditions = false, ): Promise { const { pdf, promise } = this.createPdfWithBase64Promise(); + const isRealUnit = brand === PdfBrand.REALUNIT; + const lang = typeof language === 'string' ? language.toLowerCase() : language; PdfUtil.drawLogo(pdf, brand, LogoSize.LARGE); this.drawSenderAddress(pdf, brand); this.drawDebtorAddress(pdf, billData.debtor, debtorName); this.drawTitle(pdf, tableData.title); - // Date + // Date (+ time for RealUnit) + const d = tableData.date; + const creditorCity = billData.creditor?.city || 'Zug'; + const dateStr = `${d.getDate()}.${d.getMonth() + 1}.${d.getFullYear()}`; + const timeStr = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; + pdf.fontSize(11); pdf.font('Helvetica'); - pdf.text(`Zug ${tableData.date.getDate()}.${tableData.date.getMonth() + 1}.${tableData.date.getFullYear()}`, { + pdf.text(isRealUnit ? `${creditorCity}, ${dateStr} ${timeStr}` : `Zug ${dateStr}`, { align: 'right', width: mm2pt(170), }); - // Table + // Description key: use RealUnit-specific key when applicable + const descriptionKey = isRealUnit + ? `invoice.realunit_receipt.${transactionType.toLowerCase()}_description` + : `invoice.table.position_row.${transactionType.toLowerCase()}_description`; + + // Table columns vary: RealUnit adds a unit price column + const hasUnitPrice = isRealUnit && tableData.unitPrice != null; + const headerColumns: PDFColumn[] = [ + { + text: this.translate('invoice.table.headers.quantity', lang) + (bankInfo ? ' *' : ''), + width: hasUnitPrice ? mm2pt(25) : mm2pt(40), + }, + { + text: this.translate('invoice.table.headers.description', lang), + }, + ]; + if (hasUnitPrice) { + headerColumns.push({ + text: this.translate('invoice.realunit_receipt.unit_price_label', lang), + width: mm2pt(30), + }); + } + headerColumns.push({ + text: this.translate('invoice.table.headers.total', lang), + width: mm2pt(30), + }); + + const dataColumns: PDFColumn[] = [ + { + text: `${tableData.quantity}`, + width: hasUnitPrice ? mm2pt(25) : mm2pt(40), + }, + { + text: this.translate(descriptionKey, lang, tableData.description), + }, + ]; + if (hasUnitPrice) { + dataColumns.push({ + text: `${billData.currency} ${tableData.unitPrice.toFixed(2)}`, + width: mm2pt(30), + }); + } + dataColumns.push({ + text: `${billData.currency} ${tableData.fiatAmount.toFixed(2)}`, + width: mm2pt(30), + }); + + const emptyCol = (w: number): PDFColumn => ({ text: '', width: mm2pt(w) }); + const qtyEmptyWidth = hasUnitPrice ? 25 : 40; + + // Table rows const rows: PDFRow[] = [ { backgroundColor: '#4A4D51', - columns: [ - { - text: this.translate('invoice.table.headers.quantity', language) + (bankInfo ? ' *' : ''), - width: mm2pt(40), - }, - { - text: this.translate('invoice.table.headers.description', language), - }, - { - text: this.translate('invoice.table.headers.total', language), - width: mm2pt(30), - }, - ], + columns: headerColumns, fontName: 'Helvetica-Bold', height: 20, padding: 5, @@ -270,35 +337,17 @@ export class SwissQRService { verticalAlign: 'center', }, { - columns: [ - { - text: `${tableData.quantity}`, - width: mm2pt(40), - }, - { - text: this.translate( - `invoice.table.position_row.${transactionType.toLowerCase()}_description`, - language, - tableData.description, - ), - }, - { - text: `${billData.currency} ${tableData.fiatAmount.toFixed(2)}`, - width: mm2pt(30), - }, - ], + columns: dataColumns, padding: 5, }, { columns: [ - { - text: '', - width: mm2pt(40), - }, + emptyCol(qtyEmptyWidth), { fontName: 'Helvetica-Bold', - text: this.translate('invoice.table.total_row.total_label', language), + text: this.translate('invoice.table.total_row.total_label', lang), }, + ...(hasUnitPrice ? [emptyCol(30)] : []), { fontName: 'Helvetica-Bold', text: `${billData.currency} ${tableData.fiatAmount.toFixed(2)}`, @@ -308,69 +357,72 @@ export class SwissQRService { height: 40, padding: 5, }, - { + ]; + + // Fees row for RealUnit + if (isRealUnit) { + rows.push({ columns: [ - { - text: '', - width: mm2pt(40), - }, - { - text: this.translate('invoice.table.vat_row.vat_label', language), - }, - { - text: '0%', - width: mm2pt(30), - }, + emptyCol(qtyEmptyWidth), + { text: this.translate('invoice.realunit_receipt.fees_label', lang) }, + ...(hasUnitPrice ? [emptyCol(30)] : []), + { text: this.translate('invoice.realunit_receipt.fees_free', lang), width: mm2pt(30) }, ], padding: 5, - }, + }); + } + + // VAT rows + rows.push( { columns: [ - { - text: '', - width: mm2pt(40), - }, - { - text: this.translate('invoice.table.vat_row.vat_amount_label', language), - }, - { - text: `${billData.currency} 0.00`, - width: mm2pt(30), - }, + emptyCol(qtyEmptyWidth), + { text: this.translate('invoice.table.vat_row.vat_label', lang) }, + ...(hasUnitPrice ? [emptyCol(30)] : []), + { text: '0%', width: mm2pt(30) }, ], padding: 5, }, { columns: [ - { - text: '', - width: mm2pt(40), - }, - { - fontName: 'Helvetica-Bold', - text: this.translate( - transactionType === TransactionType.REFERRAL - ? 'invoice.table.credit_total_row.credit_total_label' - : 'invoice.table.invoice_total_row.invoice_total_label', - language, - ), - }, - { - fontName: 'Helvetica-Bold', - text: `${billData.currency} ${tableData.fiatAmount.toFixed(2)}`, - width: mm2pt(30), - }, + emptyCol(qtyEmptyWidth), + { text: this.translate('invoice.table.vat_row.vat_amount_label', lang) }, + ...(hasUnitPrice ? [emptyCol(30)] : []), + { text: `${billData.currency} 0.00`, width: mm2pt(30) }, ], - height: 40, padding: 5, }, - ]; + ); + + // Total row + const totalLabel = isRealUnit + ? this.translate('invoice.realunit_receipt.receipt_total_label', lang) + : this.translate( + transactionType === TransactionType.REFERRAL + ? 'invoice.table.credit_total_row.credit_total_label' + : 'invoice.table.invoice_total_row.invoice_total_label', + lang, + ); + rows.push({ + columns: [ + emptyCol(qtyEmptyWidth), + { fontName: 'Helvetica-Bold', text: totalLabel }, + ...(hasUnitPrice ? [emptyCol(30)] : []), + { + fontName: 'Helvetica-Bold', + text: `${billData.currency} ${tableData.fiatAmount.toFixed(2)}`, + width: mm2pt(30), + }, + ], + height: 40, + padding: 5, + }); if (bankInfo) { rows.push({ columns: [ { - text: this.translate('invoice.info', language), + text: this.translate('invoice.info', lang), textOptions: { oblique: true, lineGap: 2 }, fontSize: 10, width: mm2pt(170), @@ -380,12 +432,17 @@ export class SwissQRService { } if (!skipTermsAndConditions) { - rows.push({ columns: [this.getTermsAndConditions(language)] }); + rows.push({ columns: [this.getTermsAndConditions(lang)] }); } const table = new Table({ rows, width: mm2pt(170) }); table.attachTo(pdf); + // RealUnit details section (buyer, wallet, txHash, payment method) + if (isRealUnit && (tableData.txHash || tableData.walletAddress || tableData.buyerName)) { + this.drawReceiptDetails(pdf, tableData, lang); + } + // QR-Bill (Swiss/LI IBAN) or GiroCode (other IBANs) const isDomesticTransfer = bankInfo && Config.isDomesticIban(bankInfo.iban); if (isDomesticTransfer) { @@ -440,19 +497,68 @@ export class SwissQRService { return promise; } + private drawReceiptDetails(pdf: typeof PDFDocument.prototype, tableData: SwissQRBillTableData, lang: string): void { + const startY = pdf.y + 15; + const labelX = mm2pt(20); + + pdf.font('Helvetica-Bold').fontSize(11); + pdf.text(this.translate('invoice.realunit_receipt.details_title', lang), labelX, startY); + + const details: { label: string; value: string }[] = []; + + if (tableData.buyerName) { + details.push({ + label: this.translate('invoice.realunit_receipt.buyer_label', lang), + value: tableData.buyerName, + }); + } + + if (tableData.walletAddress) { + details.push({ + label: this.translate('invoice.realunit_receipt.wallet_label', lang), + value: tableData.walletAddress, + }); + } + + if (tableData.txHash) { + details.push({ + label: this.translate('invoice.realunit_receipt.tx_hash_label', lang), + value: tableData.txHash, + }); + } + + details.push({ + label: this.translate('invoice.realunit_receipt.payment_method_label', lang), + value: this.translate('invoice.realunit_receipt.payment_method_blockchain', lang), + }); + + pdf.fontSize(10); + let currentY = startY + 18; + for (const { label, value } of details) { + pdf.font('Helvetica-Bold').text(`${label}:`, labelX, currentY, { continued: true }); + pdf.font('Helvetica').text(` ${value}`, { width: mm2pt(150) }); + currentY = pdf.y + 4; + } + } + private generateMultiPdfInvoice( tableDataWithType: { data: SwissQRBillTableData; type: TransactionType }[], language: string, billData: QrBillData, brand: PdfBrand = PdfBrand.DFX, skipTermsAndConditions = false, + walletAddress?: string, + buyerName?: string, ): Promise { const { pdf, promise } = this.createPdfWithBase64Promise(); + const isRealUnit = brand === PdfBrand.REALUNIT; + const lang = typeof language === 'string' ? language.toLowerCase() : language; + const hasUnitPrice = isRealUnit && tableDataWithType.some((t) => t.data.unitPrice != null); PdfUtil.drawLogo(pdf, brand, LogoSize.LARGE); this.drawSenderAddress(pdf, brand); this.drawDebtorAddress(pdf, billData.debtor); - this.drawTitle(pdf, this.translate('invoice.multi_receipt_title', language)); + this.drawTitle(pdf, this.translate('invoice.multi_receipt_title', lang)); const buyTransactions = tableDataWithType.filter((t) => t.type === TransactionType.BUY); const sellTransactions = tableDataWithType.filter((t) => t.type === TransactionType.SELL); @@ -460,131 +566,120 @@ export class SwissQRService { const sellTotal = sellTransactions.reduce((sum, t) => sum + t.data.fiatAmount, 0); const rows: PDFRow[] = []; + const qtyWidth = hasUnitPrice ? 20 : 30; + const emptyCol = (w: number): PDFColumn => ({ text: '', width: mm2pt(w) }); + + const buildSectionHeader = (sectionKey: string): PDFRow => ({ + columns: [ + { + text: this.translate(`invoice.section.${sectionKey}`, lang), + fontName: 'Helvetica-Bold', + fontSize: 12, + }, + ], + height: 30, + padding: [15, 5, 5, 5], + }); - if (buyTransactions.length > 0) { - rows.push({ - columns: [ - { - text: this.translate('invoice.section.buy', language), - fontName: 'Helvetica-Bold', - fontSize: 12, - }, - ], - height: 30, - padding: [15, 5, 5, 5], - }); - - rows.push({ + const buildTableHeader = (): PDFRow => { + const cols: PDFColumn[] = [ + { text: this.translate('invoice.table.headers.quantity', lang), width: mm2pt(qtyWidth) }, + { text: this.translate('invoice.table.headers.description', lang) }, + ]; + if (hasUnitPrice) { + cols.push({ text: this.translate('invoice.realunit_receipt.unit_price_label', lang), width: mm2pt(25) }); + } + cols.push( + { text: this.translate('invoice.table.headers.date', lang), width: mm2pt(25) }, + { text: this.translate('invoice.table.headers.total', lang), width: mm2pt(30) }, + ); + return { backgroundColor: '#4A4D51', - columns: [ - { text: this.translate('invoice.table.headers.quantity', language), width: mm2pt(30) }, - { text: this.translate('invoice.table.headers.description', language) }, - { text: this.translate('invoice.table.headers.date', language), width: mm2pt(25) }, - { text: this.translate('invoice.table.headers.total', language), width: mm2pt(30) }, - ], + columns: cols, fontName: 'Helvetica-Bold', height: 20, padding: 5, textColor: '#fff', verticalAlign: 'center', - }); + }; + }; - for (const { data: tableData } of buyTransactions) { - const txDate = tableData.date; - const formattedDate = `${txDate.getDate()}.${txDate.getMonth() + 1}.${txDate.getFullYear()}`; - rows.push({ - columns: [ - { text: `${tableData.quantity}`, width: mm2pt(30) }, - { text: this.translate('invoice.table.position_row.buy_description', language, tableData.description) }, - { text: formattedDate, width: mm2pt(25) }, - { text: `${billData.currency} ${tableData.fiatAmount.toFixed(2)}`, width: mm2pt(30) }, - ], - padding: 5, - }); - } + const buildDataRow = (tableData: SwissQRBillTableData, txType: TransactionType): PDFRow => { + const txDate = tableData.date; + const formattedDate = isRealUnit + ? `${txDate.getDate()}.${txDate.getMonth() + 1}.${txDate.getFullYear()} ${String(txDate.getHours()).padStart(2, '0')}:${String(txDate.getMinutes()).padStart(2, '0')}` + : `${txDate.getDate()}.${txDate.getMonth() + 1}.${txDate.getFullYear()}`; - rows.push({ - columns: [ - { text: '', width: mm2pt(30) }, - { - text: this.translate('invoice.table.total_row.total_label', language), - fontName: 'Helvetica-Bold', - }, - { text: '', width: mm2pt(25) }, - { text: `${billData.currency} ${buyTotal.toFixed(2)}`, width: mm2pt(30), fontName: 'Helvetica-Bold' }, - ], - height: 25, - padding: 5, - }); - } + const descKey = isRealUnit + ? `invoice.realunit_receipt.${txType.toLowerCase()}_description` + : `invoice.table.position_row.${txType.toLowerCase()}_description`; - if (sellTransactions.length > 0) { - rows.push({ - columns: [ - { - text: this.translate('invoice.section.sell', language), - fontName: 'Helvetica-Bold', - fontSize: 12, - }, - ], - height: 30, - padding: [15, 5, 5, 5], - }); + const cols: PDFColumn[] = [ + { text: `${tableData.quantity}`, width: mm2pt(qtyWidth) }, + { text: this.translate(descKey, lang, tableData.description) }, + ]; + if (hasUnitPrice) { + cols.push({ + text: tableData.unitPrice != null ? `${billData.currency} ${tableData.unitPrice.toFixed(2)}` : '', + width: mm2pt(25), + }); + } + cols.push( + { text: formattedDate, width: mm2pt(25) }, + { text: `${billData.currency} ${tableData.fiatAmount.toFixed(2)}`, width: mm2pt(30) }, + ); + return { columns: cols, padding: 5 }; + }; - rows.push({ - backgroundColor: '#4A4D51', - columns: [ - { text: this.translate('invoice.table.headers.quantity', language), width: mm2pt(30) }, - { text: this.translate('invoice.table.headers.description', language) }, - { text: this.translate('invoice.table.headers.date', language), width: mm2pt(25) }, - { text: this.translate('invoice.table.headers.total', language), width: mm2pt(30) }, - ], + const buildSubtotalRow = (total: number): PDFRow => { + const cols: PDFColumn[] = [ + emptyCol(qtyWidth), + { text: this.translate('invoice.table.total_row.total_label', lang), fontName: 'Helvetica-Bold' }, + ]; + if (hasUnitPrice) cols.push(emptyCol(25)); + cols.push(emptyCol(25), { + text: `${billData.currency} ${total.toFixed(2)}`, + width: mm2pt(30), fontName: 'Helvetica-Bold', - height: 20, - padding: 5, - textColor: '#fff', - verticalAlign: 'center', }); + return { columns: cols, height: 25, padding: 5 }; + }; - for (const { data: tableData } of sellTransactions) { - const txDate = tableData.date; - const formattedDate = `${txDate.getDate()}.${txDate.getMonth() + 1}.${txDate.getFullYear()}`; - rows.push({ - columns: [ - { text: `${tableData.quantity}`, width: mm2pt(30) }, - { - text: this.translate('invoice.table.position_row.sell_description', language, tableData.description), - }, - { text: formattedDate, width: mm2pt(25) }, - { text: `${billData.currency} ${tableData.fiatAmount.toFixed(2)}`, width: mm2pt(30) }, - ], - padding: 5, - }); - } + if (buyTransactions.length > 0) { + rows.push(buildSectionHeader('buy')); + rows.push(buildTableHeader()); + for (const { data, type } of buyTransactions) rows.push(buildDataRow(data, type)); + rows.push(buildSubtotalRow(buyTotal)); + } - // Sell subtotal - rows.push({ - columns: [ - { text: '', width: mm2pt(30) }, - { - text: this.translate('invoice.table.total_row.total_label', language), - fontName: 'Helvetica-Bold', - }, - { text: '', width: mm2pt(25) }, - { text: `${billData.currency} ${sellTotal.toFixed(2)}`, width: mm2pt(30), fontName: 'Helvetica-Bold' }, - ], - height: 25, - padding: 5, - }); + if (sellTransactions.length > 0) { + rows.push(buildSectionHeader('sell')); + rows.push(buildTableHeader()); + for (const { data, type } of sellTransactions) rows.push(buildDataRow(data, type)); + rows.push(buildSubtotalRow(sellTotal)); } if (!skipTermsAndConditions) { - rows.push({ columns: [this.getTermsAndConditions(language)] }); + rows.push({ columns: [this.getTermsAndConditions(lang)] }); } const table = new Table({ rows, width: mm2pt(170) }); table.attachTo(pdf); + // RealUnit details section + if (isRealUnit && (walletAddress || buyerName)) { + const detailsData: SwissQRBillTableData = { + title: '', + quantity: 0, + description: {}, + fiatAmount: 0, + date: new Date(), + walletAddress, + buyerName, + }; + this.drawReceiptDetails(pdf, detailsData, lang); + } + pdf.end(); return promise; diff --git a/src/subdomains/supporting/realunit/controllers/realunit.controller.ts b/src/subdomains/supporting/realunit/controllers/realunit.controller.ts index b4c3e4b663..ce1b5e20ce 100644 --- a/src/subdomains/supporting/realunit/controllers/realunit.controller.ts +++ b/src/subdomains/supporting/realunit/controllers/realunit.controller.ts @@ -200,6 +200,8 @@ export class RealUnitController { currency, isIncoming, PdfBrand.REALUNIT, + dto.language, + jwt.address, ); return { pdfData }; @@ -241,6 +243,8 @@ export class RealUnitController { realuAsset, currency, PdfBrand.REALUNIT, + dto.language, + jwt.address, ); return { pdfData }; diff --git a/src/subdomains/supporting/realunit/dto/realunit-pdf.dto.ts b/src/subdomains/supporting/realunit/dto/realunit-pdf.dto.ts index 37e231e1e4..21a52426f3 100644 --- a/src/subdomains/supporting/realunit/dto/realunit-pdf.dto.ts +++ b/src/subdomains/supporting/realunit/dto/realunit-pdf.dto.ts @@ -54,6 +54,11 @@ export class RealUnitSingleReceiptPdfDto { @IsOptional() @IsEnum(ReceiptCurrency) currency?: ReceiptCurrency = ReceiptCurrency.CHF; + + @ApiPropertyOptional({ description: 'Language for the receipt', enum: PdfLanguage }) + @IsOptional() + @IsEnum(PdfLanguage) + language?: PdfLanguage; } export class RealUnitMultiReceiptPdfDto { @@ -71,4 +76,9 @@ export class RealUnitMultiReceiptPdfDto { @IsOptional() @IsEnum(ReceiptCurrency) currency?: ReceiptCurrency = ReceiptCurrency.CHF; + + @ApiPropertyOptional({ description: 'Language for the receipt', enum: PdfLanguage }) + @IsOptional() + @IsEnum(PdfLanguage) + language?: PdfLanguage; }