Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export class BuyController {
@ApiOkResponse({ type: PdfDto })
async generateInvoicePDF(@GetJwt() jwt: JwtPayload, @Param('id') id: string): Promise<PdfDto> {
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');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
55 changes: 38 additions & 17 deletions src/subdomains/supporting/payment/services/swiss-qr.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -82,15 +81,22 @@ 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(
{ statementType, transactionType, transaction, currency, bankInfo, reference, request }: TxStatementDetails,
brand: PdfBrand = PdfBrand.DFX,
): Promise<string> {
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)) {
Expand All @@ -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(
Expand All @@ -121,6 +135,7 @@ export class SwissQRService {
includeQrBill: boolean,
transactionType: TransactionType,
brand: PdfBrand = PdfBrand.DFX,
debtorName?: string,
): Promise<string> {
return new Promise((resolve, reject) => {
try {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading