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; }