Skip to content

Commit 78d4141

Browse files
authored
Merge pull request #50 from topcoder-platform/PM-1217_prevent-duplicate-payments
PM-1217 prevent duplicate payments
2 parents ace3c2c + 70a4cb6 commit 78d4141

File tree

1 file changed

+69
-54
lines changed

1 file changed

+69
-54
lines changed

src/api/withdrawal/withdrawal.service.ts

Lines changed: 69 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ENV_CONFIG } from 'src/config';
33
import { PrismaService } from 'src/shared/global/prisma.service';
44
import { TaxFormRepository } from '../repository/taxForm.repo';
55
import { PaymentMethodRepository } from '../repository/paymentMethod.repo';
6-
import { payment_releases, payment_status, PrismaClient } from '@prisma/client';
6+
import { payment_releases, payment_status, Prisma } from '@prisma/client';
77
import { TrolleyService } from 'src/shared/global/trolley.service';
88
import { PaymentsService } from 'src/shared/payments';
99

@@ -42,12 +42,14 @@ export class WithdrawalService {
4242
private async getReleasableWinningsForUserId(
4343
userId: string,
4444
winningsIds: string[],
45+
tx: Prisma.TransactionClient,
4546
) {
46-
const winnings = await this.prisma.$queryRaw<ReleasableWinningRow[]>`
47+
const winnings = await tx.$queryRaw<ReleasableWinningRow[]>`
4748
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"
4849
FROM payment p INNER JOIN winnings w on p.winnings_id = w.winning_id
4950
AND p.installment_number = 1
5051
WHERE p.winnings_id = ANY(${winningsIds}::uuid[]) AND w.winner_id = ${userId}
52+
FOR UPDATE NOWAIT
5153
`;
5254

5355
if (winnings.length < winningsIds.length) {
@@ -91,7 +93,7 @@ export class WithdrawalService {
9193
}
9294

9395
private async createDbPaymentRelease(
94-
tx: PrismaClient,
96+
tx: Prisma.TransactionClient,
9597
userId: string,
9698
totalAmount: number,
9799
paymentMethodId: number,
@@ -129,7 +131,7 @@ export class WithdrawalService {
129131
}
130132

131133
private async updateDbReleaseRecord(
132-
tx: PrismaClient,
134+
tx: Prisma.TransactionClient,
133135
paymentRelease: payment_releases,
134136
externalTxId: string,
135137
) {
@@ -168,67 +170,80 @@ export class WithdrawalService {
168170
);
169171
}
170172

171-
const winnings = await this.getReleasableWinningsForUserId(
172-
userId,
173-
winningsIds,
174-
);
175-
176-
const totalAmount = this.checkTotalAmount(winnings);
173+
try {
174+
await this.prisma.$transaction(async (tx) => {
175+
const winnings = await this.getReleasableWinningsForUserId(
176+
userId,
177+
winningsIds,
178+
tx,
179+
);
177180

178-
this.logger.log('Begin processing payments', winnings);
179-
await this.prisma.$transaction(async (tx) => {
180-
const recipient = await this.getTrolleyRecipientByUserId(userId);
181+
const totalAmount = this.checkTotalAmount(winnings);
181182

182-
if (!recipient) {
183-
throw new Error(`Trolley recipient not found for user '${userId}'!`);
184-
}
183+
this.logger.log('Begin processing payments', winnings);
185184

186-
const paymentRelease = await this.createDbPaymentRelease(
187-
tx as PrismaClient,
188-
userId,
189-
totalAmount,
190-
connectedPaymentMethod.payment_method_id,
191-
recipient.trolley_id,
192-
winnings,
193-
);
185+
const recipient = await this.getTrolleyRecipientByUserId(userId);
194186

195-
const paymentBatch = await this.trolleyService.startBatchPayment(
196-
`${userId}_${userHandle}`,
197-
);
187+
if (!recipient) {
188+
throw new Error(`Trolley recipient not found for user '${userId}'!`);
189+
}
198190

199-
const trolleyPayment = await this.trolleyService.createPayment(
200-
recipient.trolley_id,
201-
paymentBatch.id,
202-
totalAmount,
203-
paymentRelease.payment_release_id,
204-
);
191+
const paymentRelease = await this.createDbPaymentRelease(
192+
tx,
193+
userId,
194+
totalAmount,
195+
connectedPaymentMethod.payment_method_id,
196+
recipient.trolley_id,
197+
winnings,
198+
);
205199

206-
await this.updateDbReleaseRecord(
207-
tx as PrismaClient,
208-
paymentRelease,
209-
trolleyPayment.id,
210-
);
200+
const paymentBatch = await this.trolleyService.startBatchPayment(
201+
`${userId}_${userHandle}`,
202+
);
211203

212-
try {
213-
await this.paymentsService.updatePaymentProcessingState(
214-
winningsIds,
215-
payment_status.PROCESSING,
216-
tx,
204+
const trolleyPayment = await this.trolleyService.createPayment(
205+
recipient.trolley_id,
206+
paymentBatch.id,
207+
totalAmount,
208+
paymentRelease.payment_release_id,
217209
);
218-
} catch (e) {
210+
211+
await this.updateDbReleaseRecord(tx, paymentRelease, trolleyPayment.id);
212+
213+
try {
214+
await this.paymentsService.updatePaymentProcessingState(
215+
winningsIds,
216+
payment_status.PROCESSING,
217+
tx,
218+
);
219+
} catch (e) {
220+
this.logger.error(
221+
`Failed to update payment processing state: ${e.message} for winnings '${winningsIds.join(',')}`,
222+
);
223+
throw new Error('Failed to update payment processing state!');
224+
}
225+
226+
try {
227+
await this.trolleyService.startProcessingPayment(paymentBatch.id);
228+
} catch (error) {
229+
const errorMsg = `Failed to release payment: ${error.message}`;
230+
this.logger.error(errorMsg, error);
231+
throw new Error(errorMsg);
232+
}
233+
});
234+
} catch (error) {
235+
if (error.code === 'P2010' && error.meta?.code === '55P03') {
219236
this.logger.error(
220-
`Failed to update payment processing state: ${e.message} for winnings '${winningsIds.join(',')}`,
237+
'Payment request denied because payment row was locked previously!',
238+
error,
221239
);
222-
throw new Error('Failed to update payment processing state!');
223-
}
224240

225-
try {
226-
await this.trolleyService.startProcessingPayment(paymentBatch.id);
227-
} catch (error) {
228-
const errorMsg = `Failed to release payment: ${error.message}`;
229-
this.logger.error(errorMsg, error);
230-
throw new Error(errorMsg);
241+
throw new Error(
242+
'Some or all of the winnings you requested to process are either processing, on hold or already paid.',
243+
);
244+
} else {
245+
throw error;
231246
}
232-
});
247+
}
233248
}
234249
}

0 commit comments

Comments
 (0)