From 2d3815520f30190774f20a1d1599166a0bcf62d8 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 28 May 2025 14:58:22 +0300 Subject: [PATCH 1/3] PM-1279 - collect paypal fees --- .../trolley/handlers/payment.handler.ts | 4 +- src/api/withdrawal/withdrawal.service.ts | 73 ++++++++++++++++--- src/config/config.env.ts | 13 ++++ src/shared/global/trolley.service.ts | 2 +- 4 files changed, 79 insertions(+), 13 deletions(-) diff --git a/src/api/webhooks/trolley/handlers/payment.handler.ts b/src/api/webhooks/trolley/handlers/payment.handler.ts index e49ce76..e12a0a0 100644 --- a/src/api/webhooks/trolley/handlers/payment.handler.ts +++ b/src/api/webhooks/trolley/handlers/payment.handler.ts @@ -57,8 +57,8 @@ export class PaymentHandler { const winningIds = ( await this.prisma.$queryRaw<{ id: string }[]>` SELECT winnings_id as id - FROM public.payment p - INNER JOIN public.payment_release_associations pra + FROM payment p + INNER JOIN payment_release_associations pra ON pra.payment_id = p.payment_id WHERE pra.payment_release_id::text = ${paymentId} FOR UPDATE diff --git a/src/api/withdrawal/withdrawal.service.ts b/src/api/withdrawal/withdrawal.service.ts index 8bd6b9c..01e356c 100644 --- a/src/api/withdrawal/withdrawal.service.ts +++ b/src/api/withdrawal/withdrawal.service.ts @@ -52,7 +52,7 @@ export class WithdrawalService { private readonly tcMembersService: TopcoderMembersService, ) {} - getTrolleyRecipientByUserId(userId: string) { + getDbTrolleyRecipientByUserId(userId: string) { return this.prisma.trolley_recipient.findUnique({ where: { user_id: userId }, }); @@ -118,6 +118,7 @@ export class WithdrawalService { paymentMethodId: number, recipientId: string, winnings: ReleasableWinningRow[], + metadata: any, ) { try { const paymentRelease = await tx.payment_releases.create({ @@ -127,6 +128,7 @@ export class WithdrawalService { status: payment_status.PROCESSING, payment_method_id: paymentMethodId, payee_id: recipientId, + metadata, payment_release_associations: { createMany: { data: winnings.map((w) => ({ @@ -176,7 +178,9 @@ export class WithdrawalService { winningsIds: string[], paymentMemo?: string, ) { - this.logger.log('Processing withdrawal request'); + this.logger.log( + `Processing withdrawal request for user ${userHandle}(${userId}), winnings: ${winningsIds.join(', ')}`, + ); const hasActiveTaxForm = await this.taxFormRepo.hasActiveTaxForm(userId); if (!hasActiveTaxForm) { @@ -206,6 +210,9 @@ export class WithdrawalService { } if (userInfo.email.toLowerCase().indexOf('wipro.com') > -1) { + this.logger.error( + `User ${userHandle}(${userId}) attempted withdrawal but is restricted due to email domain '${userInfo.email}'.`, + ); throw new Error( 'Please contact Topgear support to process your withdrawal.', ); @@ -219,23 +226,69 @@ export class WithdrawalService { tx, ); + this.logger.log( + `Begin processing payments for user ${userHandle}(${userId})`, + winnings, + ); + + const dbTrolleyRecipient = + await this.getDbTrolleyRecipientByUserId(userId); + + if (!dbTrolleyRecipient) { + throw new Error( + `Trolley recipient not found for user ${userHandle}(${userId})!`, + ); + } + const totalAmount = this.checkTotalAmount(winnings); + let paymentAmount = totalAmount; + let feeAmount = 0; + const trolleyRecipientPayoutDetails = + await this.trolleyService.getRecipientPayoutDetails( + dbTrolleyRecipient.trolley_id, + ); - this.logger.log('Begin processing payments', winnings); + if (!trolleyRecipientPayoutDetails) { + throw new Error( + `Recipient payout details not found for Trolley Recipient ID '${dbTrolleyRecipient.trolley_id}', for user ${userHandle}(${userId}).`, + ); + } - const recipient = await this.getTrolleyRecipientByUserId(userId); + if ( + trolleyRecipientPayoutDetails.payoutMethod === 'paypal' && + ENV_CONFIG.TROLLEY_PAYPAL_FEE_PERCENT + ) { + const feePercent = + 1 - 1 / (1 + Number(ENV_CONFIG.TROLLEY_PAYPAL_FEE_PERCENT) / 100); + feeAmount = Math.max( + ENV_CONFIG.TROLLEY_PAYPAL_FEE_MAX_AMOUNT, + feePercent * paymentAmount, + ); - if (!recipient) { - throw new Error(`Trolley recipient not found for user '${userId}'!`); + paymentAmount -= feeAmount; } + this.logger.log( + ` + Total amount won: $${totalAmount} USD, to be paid: $${totalAmount.toFixed(2)} USD. + Fee applied: $${feeAmount.toFixed(2)} USD (${Number(ENV_CONFIG.TROLLEY_PAYPAL_FEE_PERCENT) * 100}%, max ${ENV_CONFIG.TROLLEY_PAYPAL_FEE_MAX_AMOUNT}). + Payout method type: ${trolleyRecipientPayoutDetails.payoutMethod}. + `, + ); + const paymentRelease = await this.createDbPaymentRelease( tx, userId, - totalAmount, + paymentAmount, connectedPaymentMethod.payment_method_id, - recipient.trolley_id, + dbTrolleyRecipient.trolley_id, winnings, + { + netAmount: paymentAmount, + feeAmount, + totalAmount: totalAmount, + payoutMethod: trolleyRecipientPayoutDetails.payoutMethod, + }, ); const paymentBatch = await this.trolleyService.startBatchPayment( @@ -243,9 +296,9 @@ export class WithdrawalService { ); const trolleyPayment = await this.trolleyService.createPayment( - recipient.trolley_id, + dbTrolleyRecipient.trolley_id, paymentBatch.id, - totalAmount, + paymentAmount, paymentRelease.payment_release_id, paymentMemo, ); diff --git a/src/config/config.env.ts b/src/config/config.env.ts index b57660f..d2062cf 100644 --- a/src/config/config.env.ts +++ b/src/config/config.env.ts @@ -3,8 +3,11 @@ import { IsBoolean, IsInt, IsNotEmpty, + IsNumber, IsOptional, IsString, + Max, + Min, } from 'class-validator'; export class ConfigEnv { @@ -90,4 +93,14 @@ export class ConfigEnv { @IsString() TOPCODER_WALLET_URL = 'https://wallet.topcoder.com'; + + @IsInt() + @Min(0) + @Max(99) + @IsOptional() + TROLLEY_PAYPAL_FEE_PERCENT: number; + + @IsNumber() + @IsOptional() + TROLLEY_PAYPAL_FEE_MAX_AMOUNT: number; } diff --git a/src/shared/global/trolley.service.ts b/src/shared/global/trolley.service.ts index eee2307..c6c7ae2 100644 --- a/src/shared/global/trolley.service.ts +++ b/src/shared/global/trolley.service.ts @@ -191,7 +191,7 @@ export class TrolleyService { ]) as RecipientTaxDetails; } catch (error) { this.logger.error( - 'Failed to load recipient tax details from trolley!', + 'Failed to load recipient tax & payout details from trolley!', error, ); return {} as RecipientTaxDetails; From 129e1ea8ba0d63b14e41b8749667cda4689ff862 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 28 May 2025 14:59:53 +0300 Subject: [PATCH 2/3] fee calculation --- src/api/withdrawal/withdrawal.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/api/withdrawal/withdrawal.service.ts b/src/api/withdrawal/withdrawal.service.ts index 01e356c..4c9ba04 100644 --- a/src/api/withdrawal/withdrawal.service.ts +++ b/src/api/withdrawal/withdrawal.service.ts @@ -258,8 +258,7 @@ export class WithdrawalService { trolleyRecipientPayoutDetails.payoutMethod === 'paypal' && ENV_CONFIG.TROLLEY_PAYPAL_FEE_PERCENT ) { - const feePercent = - 1 - 1 / (1 + Number(ENV_CONFIG.TROLLEY_PAYPAL_FEE_PERCENT) / 100); + const feePercent = Number(ENV_CONFIG.TROLLEY_PAYPAL_FEE_PERCENT) / 100; feeAmount = Math.max( ENV_CONFIG.TROLLEY_PAYPAL_FEE_MAX_AMOUNT, feePercent * paymentAmount, From 73a3a1f03b9b926f6a399591793f1cccb3e33487 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 28 May 2025 17:08:13 +0300 Subject: [PATCH 3/3] fix paypal fee collection --- src/api/withdrawal/withdrawal.service.ts | 16 +++++++++++----- src/config/config.env.ts | 4 ++-- src/shared/global/trolley.service.ts | 4 ++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/api/withdrawal/withdrawal.service.ts b/src/api/withdrawal/withdrawal.service.ts index 4c9ba04..c3a12d9 100644 --- a/src/api/withdrawal/withdrawal.service.ts +++ b/src/api/withdrawal/withdrawal.service.ts @@ -258,19 +258,21 @@ export class WithdrawalService { trolleyRecipientPayoutDetails.payoutMethod === 'paypal' && ENV_CONFIG.TROLLEY_PAYPAL_FEE_PERCENT ) { - const feePercent = Number(ENV_CONFIG.TROLLEY_PAYPAL_FEE_PERCENT) / 100; - feeAmount = Math.max( + const feePercent = + Number(ENV_CONFIG.TROLLEY_PAYPAL_FEE_PERCENT) / 100; + + feeAmount = +Math.min( ENV_CONFIG.TROLLEY_PAYPAL_FEE_MAX_AMOUNT, feePercent * paymentAmount, - ); + ).toFixed(2); paymentAmount -= feeAmount; } this.logger.log( ` - Total amount won: $${totalAmount} USD, to be paid: $${totalAmount.toFixed(2)} USD. - Fee applied: $${feeAmount.toFixed(2)} USD (${Number(ENV_CONFIG.TROLLEY_PAYPAL_FEE_PERCENT) * 100}%, max ${ENV_CONFIG.TROLLEY_PAYPAL_FEE_MAX_AMOUNT}). + Total amount won: $${totalAmount.toFixed(2)} USD, to be paid: $${paymentAmount.toFixed(2)} USD. + Fee applied: $${feeAmount.toFixed(2)} USD (${Number(ENV_CONFIG.TROLLEY_PAYPAL_FEE_PERCENT)}%, max ${ENV_CONFIG.TROLLEY_PAYPAL_FEE_MAX_AMOUNT}). Payout method type: ${trolleyRecipientPayoutDetails.payoutMethod}. `, ); @@ -287,6 +289,10 @@ export class WithdrawalService { feeAmount, totalAmount: totalAmount, payoutMethod: trolleyRecipientPayoutDetails.payoutMethod, + env_trolley_paypal_fee_percent: + ENV_CONFIG.TROLLEY_PAYPAL_FEE_PERCENT, + env_trolley_paypal_fee_max_amount: + ENV_CONFIG.TROLLEY_PAYPAL_FEE_MAX_AMOUNT, }, ); diff --git a/src/config/config.env.ts b/src/config/config.env.ts index d2062cf..5e0c7a5 100644 --- a/src/config/config.env.ts +++ b/src/config/config.env.ts @@ -98,9 +98,9 @@ export class ConfigEnv { @Min(0) @Max(99) @IsOptional() - TROLLEY_PAYPAL_FEE_PERCENT: number; + TROLLEY_PAYPAL_FEE_PERCENT: number = 0; @IsNumber() @IsOptional() - TROLLEY_PAYPAL_FEE_MAX_AMOUNT: number; + TROLLEY_PAYPAL_FEE_MAX_AMOUNT: number = 0; } diff --git a/src/shared/global/trolley.service.ts b/src/shared/global/trolley.service.ts index c6c7ae2..7b88d85 100644 --- a/src/shared/global/trolley.service.ts +++ b/src/shared/global/trolley.service.ts @@ -118,8 +118,8 @@ export class TrolleyService { recipient: { id: recipientId, }, - sourceAmount: totalAmount.toString(), - sourceCurrency: 'USD', + amount: totalAmount.toFixed(2), + currency: 'USD', memo: paymentMemo ?? 'Topcoder payment', externalId: transactionId, };