Skip to content
Merged
11 changes: 1 addition & 10 deletions src/api/wallet/wallet.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,6 @@ export class WalletService {
),
unit: 'currency',
},
// hide rewards for now
// {
// type: WinningsType.REWARD,
// amount: rewardTotal,
// unit: 'points',
// },
],
},
withdrawalMethod: {
Expand Down Expand Up @@ -118,7 +112,7 @@ export class WalletService {

getWinningsTotalsByWinnerID(winnerId: string) {
return this.prisma.$queryRaw<
{ payment_type: 'PAYMENT' | 'REWARD'; total_owed: number }[]
{ payment_type: 'PAYMENT'; total_owed: number }[]

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 the payment_type union may impact the correctness of the query results if REWARD payments are still relevant to the business logic. Ensure that this change aligns with the intended functionality and that REWARD payments are handled appropriately elsewhere if needed.

>`
WITH latest_payment_version AS (
SELECT
Expand All @@ -133,7 +127,6 @@ export class WalletService {
w.type AS payment_type,
CASE
WHEN w.type = 'PAYMENT' THEN SUM(p.total_amount)
WHEN w.type = 'REWARD' THEN SUM(r.points)
ELSE 0
END AS total_owed
FROM
Expand All @@ -144,8 +137,6 @@ export class WalletService {
AND p.installment_number = 1
INNER JOIN latest_payment_version lpv ON p.winnings_id = lpv.winnings_id
AND p.version = lpv.max_version
LEFT JOIN reward r ON w.winning_id = r.winnings_id
AND w.type = 'REWARD'
WHERE
w.winner_id = ${winnerId}
GROUP BY
Expand Down
274 changes: 147 additions & 127 deletions src/api/withdrawal/withdrawal.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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[]>`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
Switching from tx.$queryRaw to this.prisma.$queryRaw changes the transaction context. Ensure that this change does not affect transaction integrity or lead to race conditions, especially in concurrent environments.

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
Expand Down Expand Up @@ -121,7 +119,6 @@ export class WithdrawalService {
}

private async createDbPaymentRelease(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The removal of the tx: Prisma.TransactionClient parameter suggests that the transaction context might no longer be passed to createDbPaymentRelease. Ensure that this change does not affect the transactional integrity of the operation, as it could lead to partial updates or data inconsistencies if the function was previously relying on a transaction context.

tx: Prisma.TransactionClient,
userId: string,
totalAmount: number,
paymentMethodId: number,
Expand All @@ -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({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
Switching from tx.payment_releases.create to this.prisma.payment_releases.create changes the context from a transaction-scoped operation to a potentially non-transactional one. Ensure that this change does not inadvertently affect transactional integrity, especially in a hotfix context where duplicated payments are a concern.

data: {
user_id: userId,
total_net_amount: totalAmount,
Expand Down Expand Up @@ -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({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
Switching from tx to this.prisma for database updates changes the transaction context. Ensure that this does not affect transaction integrity, especially if tx was part of a larger transaction scope.

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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The conditional logging based on data.externalTxId might lead to missing logs if externalTxId is not provided. Consider logging the update operation regardless of externalTxId presence for better traceability.

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);
Expand Down Expand Up @@ -229,6 +230,21 @@ export class WithdrawalService {
throw new Error('Failed to fetch UserInfo for withdrawal!');
}

if (userInfo.email.toLowerCase().indexOf('wipro.com') > -1) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The check for the email domain wipro.com is hardcoded. Consider externalizing this configuration to allow for easier updates and maintenance, especially if more domains need to be added or changed in the future.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 readability]
The use of indexOf for checking the presence of a substring can be replaced with includes for better readability and clarity.

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,
Expand All @@ -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 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The try block starting here does not have a corresponding catch block to handle potential errors from this.paymentsService.updatePaymentProcessingState. This could lead to unhandled promise rejections if an error occurs. Consider adding a catch block to handle errors appropriately.

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(',')}`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Using optional chaining (e?.message) is a good practice to avoid runtime errors when e is undefined or null. However, ensure that e is always an object or undefined in this context to prevent any unexpected behavior. Consider adding a fallback message for cases where e might not be an object.

);

const trolleyPayment = await this.trolleyService.createPayment(
dbTrolleyRecipient.trolley_id,
// mark release as failed
await this.updateDbReleaseRecord(paymentRelease, {
status: 'FAILED',
});

await this.trolleyService.removePayment(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
Consider handling errors that might occur during removePayment. If removePayment fails, it could leave the system in an inconsistent state. Ensure that any errors are logged or handled appropriately to maintain system integrity.

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 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The try block starting here does not have a corresponding catch block to handle potential errors from this.trolleyService.startProcessingPayment. This could lead to unhandled promise rejections if an error occurs. Consider adding a catch block to handle errors appropriately.

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 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The try block starting here does not have a corresponding catch block to handle potential errors from this.tcChallengesService.updateLegacyPayments. This could lead to unhandled promise rejections if an error occurs. Consider adding a catch block to handle errors appropriately.

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(
Expand Down
Loading