From c2d2b2a3f973fd7fb138a6f620bb403ac85f7705 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:31:07 +0100 Subject: [PATCH] Reduce required fields for invoice generation (#3039) * Reduce required fields for invoice generation Only require accountType and name (firstname/surname for personal, organizationName for organizations) instead of full KYC data for invoice PDF generation. * Add separate isInvoiceDataComplete property for invoice validation Separate KYC data completion from invoice data requirements: - requiredKycFields: minimal fields for KYC step completion (name + accountType) - requiredInvoiceFields: full address fields required for valid invoices - isInvoiceDataComplete: used in invoice generation endpoints This prevents TypeError when generating invoices with incomplete address data, while still allowing KYC steps to complete with minimal data. Affected files: - user-data.entity.ts: add requiredInvoiceFields and isInvoiceDataComplete - swiss-qr.service.ts: use isInvoiceDataComplete in getDebtor() - transaction-helper.ts: use isInvoiceDataComplete for invoice validation - buy.controller.ts: use isInvoiceDataComplete for invoice endpoint - transaction.controller.ts: use isInvoiceDataComplete for invoice endpoint * Fix: keep requiredKycFields unchanged, only add minimal requiredInvoiceFields - Restore requiredKycFields to original (all KYC fields) - requiredInvoiceFields now only requires accountType + name - This ensures invoice PDF can be generated with minimal data - All other functionality (sell routes, KYC status, etc.) unchanged * Fix null pointer exception when address is missing in getDebtor * Fix undefined buildingNumber in invoice PDF debtor address * Fix SwissQRBill validation error for users without address - Return undefined debtor when no valid address (country) exists - SwissQRBill library requires country to be exactly 2 characters - Add debtorName parameter to always show name on PDF - Remove "Debtor is required" checks to allow invoice generation without full address data --- .../buy-crypto/routes/buy/buy.controller.ts | 2 +- .../controllers/transaction.controller.ts | 2 +- .../user/models/user-data/user-data.entity.ts | 10 ++++ .../payment/services/swiss-qr.service.ts | 55 +++++++++++++------ .../payment/services/transaction-helper.ts | 2 +- 5 files changed, 51 insertions(+), 20 deletions(-) diff --git a/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts b/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts index 4c3c20e5c2..f48058be40 100644 --- a/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts +++ b/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts @@ -161,7 +161,7 @@ export class BuyController { @ApiOkResponse({ type: PdfDto }) async generateInvoicePDF(@GetJwt() jwt: JwtPayload, @Param('id') id: string): Promise { const request = await this.transactionRequestService.getOrThrow(+id, jwt.user); - if (!request.userData.isDataComplete) throw new BadRequestException('User data is not complete'); + if (!request.userData.isInvoiceDataComplete) throw new BadRequestException('User data is not complete'); if (!request.isValid) throw new BadRequestException('Transaction request is not valid'); if (request.isComplete) throw new ConflictException('Transaction request is already confirmed'); diff --git a/src/subdomains/core/history/controllers/transaction.controller.ts b/src/subdomains/core/history/controllers/transaction.controller.ts index 6b050012df..5e0eecb5f8 100644 --- a/src/subdomains/core/history/controllers/transaction.controller.ts +++ b/src/subdomains/core/history/controllers/transaction.controller.ts @@ -447,7 +447,7 @@ export class TransactionController { if (request) { // Validate ownership and state if (request.user.userData.id !== jwt.account) throw new ForbiddenException('Not your transaction request'); - if (!request.userData.isDataComplete) throw new BadRequestException('User data is not complete'); + if (!request.userData.isInvoiceDataComplete) throw new BadRequestException('User data is not complete'); if (!request.isValid) throw new BadRequestException('Transaction request is not valid'); // Generate invoice from request (pending transaction) diff --git a/src/subdomains/generic/user/models/user-data/user-data.entity.ts b/src/subdomains/generic/user/models/user-data/user-data.entity.ts index 58dd36249f..70c22c1cb2 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.entity.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.entity.ts @@ -748,6 +748,16 @@ export class UserData extends IEntity { return this.requiredKycFields.every((f) => this[f]); } + get requiredInvoiceFields(): string[] { + return ['accountType'].concat( + !this.accountType || this.accountType === AccountType.PERSONAL ? ['firstname', 'surname'] : ['organizationName'], + ); + } + + get isInvoiceDataComplete(): boolean { + return this.requiredInvoiceFields.every((f) => this[f]); + } + get hasBankTxVerification(): boolean { return [CheckStatus.PASS, CheckStatus.UNNECESSARY, CheckStatus.GSHEET].includes(this.bankTransactionVerification); } diff --git a/src/subdomains/supporting/payment/services/swiss-qr.service.ts b/src/subdomains/supporting/payment/services/swiss-qr.service.ts index 51319d4001..eb566113ea 100644 --- a/src/subdomains/supporting/payment/services/swiss-qr.service.ts +++ b/src/subdomains/supporting/payment/services/swiss-qr.service.ts @@ -65,7 +65,6 @@ export class SwissQRService { } const data = this.generateQrData(amount, currency, bankInfo, reference, request.userData); - if (!data.debtor) throw new Error('Debtor is required'); const userLanguage = request.userData.language.symbol.toUpperCase(); const language = this.isSupportedInvoiceLanguage(userLanguage) ? userLanguage : 'EN'; @@ -82,7 +81,15 @@ export class SwissQRService { date: request.created, }; - return this.generatePdfInvoice(tableData, language, data, true, TransactionType.BUY); + return this.generatePdfInvoice( + tableData, + language, + data, + true, + TransactionType.BUY, + PdfBrand.DFX, + request.userData.completeName, + ); } async createTxStatement( @@ -90,7 +97,6 @@ export class SwissQRService { brand: PdfBrand = PdfBrand.DFX, ): Promise { const debtor = this.getDebtor(transaction.userData); - if (!debtor) throw new Error('Debtor is required'); currency = Config.invoice.currencies.includes(currency) ? currency : Config.invoice.defaultCurrency; if (!this.isSupportedInvoiceCurrency(currency)) { @@ -111,7 +117,15 @@ export class SwissQRService { message: reference, }; - return this.generatePdfInvoice(tableData, language, billData, !!bankInfo, transactionType, brand); + return this.generatePdfInvoice( + tableData, + language, + billData, + !!bankInfo, + transactionType, + brand, + transaction.userData.completeName, + ); } private generatePdfInvoice( @@ -121,6 +135,7 @@ export class SwissQRService { includeQrBill: boolean, transactionType: TransactionType, brand: PdfBrand = PdfBrand.DFX, + debtorName?: string, ): Promise { return new Promise((resolve, reject) => { try { @@ -156,18 +171,20 @@ export class SwissQRService { ); // Debtor address - pdf.fontSize(12); - pdf.font('Helvetica'); - pdf.text( - `${billData.debtor.name}\n${billData.debtor.address} ${billData.debtor.buildingNumber}\n${billData.debtor.zip} ${billData.debtor.city}`, - mm2pt(130), - mm2pt(60), - { + const displayName = billData.debtor?.name ?? debtorName; + if (displayName) { + pdf.fontSize(12); + pdf.font('Helvetica'); + const addressLine = billData.debtor + ? [billData.debtor.address, billData.debtor.buildingNumber].filter(Boolean).join(' ') + : ''; + const cityLine = billData.debtor ? [billData.debtor.zip, billData.debtor.city].filter(Boolean).join(' ') : ''; + pdf.text([displayName, addressLine, cityLine].filter(Boolean).join('\n'), mm2pt(130), mm2pt(60), { align: 'left', height: mm2pt(50), width: mm2pt(70), - }, - ); + }); + } // Title pdf.fontSize(14); @@ -440,17 +457,21 @@ export class SwissQRService { } private getDebtor(userData?: UserData): Debtor | undefined { - if (!userData?.isDataComplete) return undefined; + if (!userData?.isInvoiceDataComplete) return undefined; const name = userData.completeName; const address = userData.address; + // SwissQRBill requires country to be exactly 2 characters + // If no valid address, return undefined (debtor is optional in QR bill) + if (!address?.country?.symbol) return undefined; + const debtor: Debtor = { name, - address: address.street, - city: address.city, + address: address.street ?? '', + city: address.city ?? '', country: address.country.symbol, - zip: address.zip, + zip: address.zip ?? '', }; if (address.houseNumber != null) debtor.buildingNumber = address.houseNumber; diff --git a/src/subdomains/supporting/payment/services/transaction-helper.ts b/src/subdomains/supporting/payment/services/transaction-helper.ts index 8d58aac16f..eb2de7212e 100644 --- a/src/subdomains/supporting/payment/services/transaction-helper.ts +++ b/src/subdomains/supporting/payment/services/transaction-helper.ts @@ -511,7 +511,7 @@ export class TransactionHelper implements OnModuleInit { : await this.transactionService.getTransactionByUid(txIdOrUid, relations); if (!transaction) throw new BadRequestException('Transaction not found'); - if (!transaction.userData.isDataComplete) throw new BadRequestException('User data is not complete'); + if (!transaction.userData.isInvoiceDataComplete) throw new BadRequestException('User data is not complete'); if (transaction.userData.id !== userDataId) throw new ForbiddenException('Not your transaction'); // Handle pending transactions (no targetEntity yet, but has request)