-
Notifications
You must be signed in to change notification settings - Fork 1
[Hotfix PROD] - duplicated payments #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
59c1e09
8fb4c09
94461a5
bce5825
42749c6
b41c474
bf2fd8d
95e072d
ac81770
120fbc5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,6 @@ import { IdentityVerificationRepository } from '../repository/identity-verificat | |
| import { | ||
| payment_releases, | ||
| payment_status, | ||
| Prisma, | ||
| reference_type, | ||
| } from '@prisma/client'; | ||
| import { TrolleyService } from 'src/shared/global/trolley.service'; | ||
|
|
@@ -70,9 +69,8 @@ export class WithdrawalService { | |
| private async getReleasableWinningsForUserId( | ||
| userId: string, | ||
| winningsIds: string[], | ||
| tx: Prisma.TransactionClient, | ||
| ) { | ||
| const winnings = await tx.$queryRaw<ReleasableWinningRow[]>` | ||
| const winnings = await this.prisma.$queryRaw<ReleasableWinningRow[]>` | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| SELECT p.payment_id as "paymentId", p.total_amount as amount, p.version, w.title, w.external_id as "externalId", p.payment_status as status, p.release_date as "releaseDate", p.date_paid as "datePaid" | ||
| FROM payment p INNER JOIN winnings w on p.winnings_id = w.winning_id | ||
| AND p.installment_number = 1 | ||
|
|
@@ -121,7 +119,6 @@ export class WithdrawalService { | |
| } | ||
|
|
||
| private async createDbPaymentRelease( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| tx: Prisma.TransactionClient, | ||
| userId: string, | ||
| totalAmount: number, | ||
| paymentMethodId: number, | ||
|
|
@@ -130,7 +127,7 @@ export class WithdrawalService { | |
| metadata: any, | ||
| ) { | ||
| try { | ||
| const paymentRelease = await tx.payment_releases.create({ | ||
| const paymentRelease = await this.prisma.payment_releases.create({ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| data: { | ||
| user_id: userId, | ||
| total_net_amount: totalAmount, | ||
|
|
@@ -161,19 +158,23 @@ export class WithdrawalService { | |
| } | ||
|
|
||
| private async updateDbReleaseRecord( | ||
| tx: Prisma.TransactionClient, | ||
| paymentRelease: payment_releases, | ||
| externalTxId: string, | ||
| data: { externalTxId?: string; status?: string }, | ||
| ) { | ||
| try { | ||
| await tx.payment_releases.update({ | ||
| await this.prisma.payment_releases.update({ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| where: { payment_release_id: paymentRelease.payment_release_id }, | ||
| data: { external_transaction_id: externalTxId }, | ||
| data: { | ||
| external_transaction_id: data.externalTxId, | ||
| status: data.status, | ||
| }, | ||
| }); | ||
|
|
||
| this.logger.log( | ||
| `DB payment_release[${paymentRelease.payment_release_id}] updated successfully with trolley payment id: ${externalTxId}`, | ||
| ); | ||
| if (data.externalTxId) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| this.logger.log( | ||
| `DB payment_release[${paymentRelease.payment_release_id}] updated successfully with trolley payment id: ${data.externalTxId}`, | ||
| ); | ||
| } | ||
| } catch (error) { | ||
| const errorMsg = `Failed to update DB payment_release: ${error.message}`; | ||
| this.logger.error(errorMsg, error); | ||
|
|
@@ -229,6 +230,21 @@ export class WithdrawalService { | |
| throw new Error('Failed to fetch UserInfo for withdrawal!'); | ||
| } | ||
|
|
||
| if (userInfo.email.toLowerCase().indexOf('wipro.com') > -1) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| 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.', | ||
| ); | ||
| } | ||
|
|
||
| // check winnings before even sending otp code | ||
| const winnings = await this.getReleasableWinningsForUserId( | ||
| userId, | ||
| winningsIds, | ||
| ); | ||
|
|
||
| if (!otpCode) { | ||
| const otpError = await this.otpService.generateOtpCode( | ||
| userInfo, | ||
|
|
@@ -247,147 +263,151 @@ 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.', | ||
| try { | ||
| this.logger.log( | ||
| `Begin processing payments for user ${userHandle}(${userId})`, | ||
| winnings, | ||
| ); | ||
| } | ||
|
|
||
| try { | ||
| await this.prisma.$transaction(async (tx) => { | ||
| const winnings = await this.getReleasableWinningsForUserId( | ||
| userId, | ||
| winningsIds, | ||
| tx, | ||
| const dbTrolleyRecipient = | ||
| await this.getDbTrolleyRecipientByUserId(userId); | ||
|
|
||
| if (!dbTrolleyRecipient) { | ||
| throw new Error( | ||
| `Trolley recipient not found for user ${userHandle}(${userId})!`, | ||
| ); | ||
| } | ||
|
|
||
| this.logger.log( | ||
| `Begin processing payments for user ${userHandle}(${userId})`, | ||
| winnings, | ||
| const totalAmount = this.checkTotalAmount(winnings); | ||
| let paymentAmount = totalAmount; | ||
| let feeAmount = 0; | ||
| const trolleyRecipientPayoutDetails = | ||
| await this.trolleyService.getRecipientPayoutDetails( | ||
| dbTrolleyRecipient.trolley_id, | ||
| ); | ||
|
|
||
| const dbTrolleyRecipient = | ||
| await this.getDbTrolleyRecipientByUserId(userId); | ||
| if (!trolleyRecipientPayoutDetails) { | ||
| throw new Error( | ||
| `Recipient payout details not found for Trolley Recipient ID '${dbTrolleyRecipient.trolley_id}', for user ${userHandle}(${userId}).`, | ||
| ); | ||
| } | ||
|
|
||
| if (!dbTrolleyRecipient) { | ||
| throw new Error( | ||
| `Trolley recipient not found for user ${userHandle}(${userId})!`, | ||
| ); | ||
| } | ||
| if ( | ||
| trolleyRecipientPayoutDetails.payoutMethod === 'paypal' && | ||
| ENV_CONFIG.TROLLEY_PAYPAL_FEE_PERCENT | ||
| ) { | ||
| const feePercent = Number(ENV_CONFIG.TROLLEY_PAYPAL_FEE_PERCENT) / 100; | ||
|
|
||
| const totalAmount = this.checkTotalAmount(winnings); | ||
| let paymentAmount = totalAmount; | ||
| let feeAmount = 0; | ||
| const trolleyRecipientPayoutDetails = | ||
| await this.trolleyService.getRecipientPayoutDetails( | ||
| dbTrolleyRecipient.trolley_id, | ||
| ); | ||
| feeAmount = +Math.min( | ||
| ENV_CONFIG.TROLLEY_PAYPAL_FEE_MAX_AMOUNT, | ||
| feePercent * paymentAmount, | ||
| ).toFixed(2); | ||
|
|
||
| if (!trolleyRecipientPayoutDetails) { | ||
| throw new Error( | ||
| `Recipient payout details not found for Trolley Recipient ID '${dbTrolleyRecipient.trolley_id}', for user ${userHandle}(${userId}).`, | ||
| ); | ||
| } | ||
| paymentAmount -= feeAmount; | ||
| } | ||
|
|
||
| if ( | ||
| trolleyRecipientPayoutDetails.payoutMethod === 'paypal' && | ||
| ENV_CONFIG.TROLLEY_PAYPAL_FEE_PERCENT | ||
| ) { | ||
| const feePercent = | ||
| Number(ENV_CONFIG.TROLLEY_PAYPAL_FEE_PERCENT) / 100; | ||
| this.logger.log( | ||
| ` | ||
| Total amount won: $${totalAmount.toFixed(2)} USD, to be paid: $${paymentAmount.toFixed(2)} USD. | ||
| Payout method type: ${trolleyRecipientPayoutDetails.payoutMethod}. | ||
| `, | ||
| ); | ||
|
|
||
| feeAmount = +Math.min( | ||
| const paymentRelease = await this.createDbPaymentRelease( | ||
| userId, | ||
| paymentAmount, | ||
| connectedPaymentMethod.payment_method_id, | ||
| dbTrolleyRecipient.trolley_id, | ||
| winnings, | ||
| { | ||
| netAmount: paymentAmount, | ||
| 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, | ||
| feePercent * paymentAmount, | ||
| ).toFixed(2); | ||
| }, | ||
| ); | ||
|
|
||
| paymentAmount -= feeAmount; | ||
| } | ||
| const paymentBatch = await this.trolleyService.startBatchPayment( | ||
| `${userId}_${userHandle}`, | ||
| ); | ||
|
|
||
| this.logger.log( | ||
| ` | ||
| Total amount won: $${totalAmount.toFixed(2)} USD, to be paid: $${paymentAmount.toFixed(2)} USD. | ||
| Payout method type: ${trolleyRecipientPayoutDetails.payoutMethod}. | ||
| `, | ||
| ); | ||
| const trolleyPayment = await this.trolleyService.createPayment( | ||
| dbTrolleyRecipient.trolley_id, | ||
| paymentBatch.id, | ||
| paymentAmount, | ||
| paymentRelease.payment_release_id, | ||
| paymentMemo, | ||
| ); | ||
|
|
||
| const paymentRelease = await this.createDbPaymentRelease( | ||
| tx, | ||
| userId, | ||
| paymentAmount, | ||
| connectedPaymentMethod.payment_method_id, | ||
| dbTrolleyRecipient.trolley_id, | ||
| winnings, | ||
| { | ||
| netAmount: paymentAmount, | ||
| 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, | ||
| }, | ||
| ); | ||
| await this.updateDbReleaseRecord(paymentRelease, { | ||
| externalTxId: trolleyPayment.id, | ||
| }); | ||
|
|
||
| const paymentBatch = await this.trolleyService.startBatchPayment( | ||
| `${userId}_${userHandle}`, | ||
| try { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| await this.paymentsService.updatePaymentProcessingState( | ||
| winningsIds, | ||
| payment_status.PROCESSING, | ||
| ); | ||
| } catch (e) { | ||
| this.logger.error( | ||
| `Failed to update payment processing state: ${e?.message} for winnings '${winningsIds.join(',')}`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| ); | ||
|
|
||
| const trolleyPayment = await this.trolleyService.createPayment( | ||
| dbTrolleyRecipient.trolley_id, | ||
| // mark release as failed | ||
| await this.updateDbReleaseRecord(paymentRelease, { | ||
| status: 'FAILED', | ||
| }); | ||
|
|
||
| await this.trolleyService.removePayment( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| trolleyPayment.id, | ||
| paymentBatch.id, | ||
| paymentAmount, | ||
| paymentRelease.payment_release_id, | ||
| paymentMemo, | ||
| ); | ||
|
|
||
| await this.updateDbReleaseRecord(tx, paymentRelease, trolleyPayment.id); | ||
| throw new Error('Failed to update payment processing state!'); | ||
| } | ||
|
|
||
| try { | ||
| await this.paymentsService.updatePaymentProcessingState( | ||
| winningsIds, | ||
| payment_status.PROCESSING, | ||
| tx, | ||
| ); | ||
| } catch (e) { | ||
| this.logger.error( | ||
| `Failed to update payment processing state: ${e.message} for winnings '${winningsIds.join(',')}`, | ||
| ); | ||
| throw new Error('Failed to update payment processing state!'); | ||
| } | ||
| try { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| await this.trolleyService.startProcessingPayment(paymentBatch.id); | ||
| } catch (error) { | ||
| const errorMsg = `Failed to release payment: ${error.message}`; | ||
| this.logger.error(errorMsg, error); | ||
|
|
||
| try { | ||
| await this.trolleyService.startProcessingPayment(paymentBatch.id); | ||
| } catch (error) { | ||
| const errorMsg = `Failed to release payment: ${error.message}`; | ||
| this.logger.error(errorMsg, error); | ||
| throw new Error(errorMsg); | ||
| } | ||
| // revert to owed | ||
| await this.paymentsService.updatePaymentProcessingState( | ||
| winningsIds, | ||
| payment_status.OWED, | ||
| ); | ||
|
|
||
| // mark release as failed | ||
| await this.updateDbReleaseRecord(paymentRelease, { | ||
| status: 'FAILED', | ||
| }); | ||
|
|
||
| try { | ||
| for (const winning of winnings) { | ||
| const payoutData: WithdrawUpdateData = { | ||
| userId: +userId, | ||
| status: 'Paid', | ||
| datePaid: formatDate(new Date()), | ||
| }; | ||
|
|
||
| await this.tcChallengesService.updateLegacyPayments( | ||
| winning.externalId as string, | ||
| payoutData, | ||
| ); | ||
| } | ||
| } catch (error) { | ||
| this.logger.error( | ||
| `Failed to update legacy payment while withdrawing for challenge ${error?.message ?? error}`, | ||
| error, | ||
| throw new Error(errorMsg); | ||
| } | ||
|
|
||
| try { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| for (const winning of winnings) { | ||
| const payoutData: WithdrawUpdateData = { | ||
| userId: +userId, | ||
| status: 'Paid', | ||
| datePaid: formatDate(new Date()), | ||
| }; | ||
|
|
||
| await this.tcChallengesService.updateLegacyPayments( | ||
| winning.externalId as string, | ||
| payoutData, | ||
| ); | ||
| } | ||
| }); | ||
| } catch (error) { | ||
| this.logger.error( | ||
| `Failed to update legacy payment while withdrawing for challenge ${error?.message ?? error}`, | ||
| error, | ||
| ); | ||
| } | ||
| } catch (error) { | ||
| if (error.code === 'P2010' && error.meta?.code === '55P03') { | ||
| this.logger.error( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[❗❗
correctness]The removal of
'REWARD'from thepayment_typeunion may impact the correctness of the query results ifREWARDpayments are still relevant to the business logic. Ensure that this change aligns with the intended functionality and thatREWARDpayments are handled appropriately elsewhere if needed.