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)