Skip to content

Commit 303df82

Browse files
authored
Merge pull request #39 from topcoder-platform/PM-1161_payment-status-sync-service
PM-1161 - reconcile payments on user details updates
2 parents 1f642f2 + d5fd8a3 commit 303df82

File tree

10 files changed

+143
-55
lines changed

10 files changed

+143
-55
lines changed

src/api/admin/admin.module.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import { TaxFormRepository } from '../repository/taxForm.repo';
55
import { PaymentMethodRepository } from '../repository/paymentMethod.repo';
66
import { WinningsRepository } from '../repository/winnings.repo';
77
import { TopcoderModule } from 'src/shared/topcoder/topcoder.module';
8+
import { PaymentsModule } from 'src/shared/payments';
89

910
@Module({
10-
imports: [TopcoderModule],
11+
imports: [TopcoderModule, PaymentsModule],
1112
controllers: [AdminController],
1213
providers: [
1314
AdminService,

src/api/admin/admin.service.ts

+3-46
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77

88
import { PrismaPromise } from '@prisma/client';
99
import { PrismaService } from 'src/shared/global/prisma.service';
10+
import { PaymentsService } from 'src/shared/payments';
1011

1112
import { TaxFormRepository } from '../repository/taxForm.repo';
1213
import { PaymentMethodRepository } from '../repository/paymentMethod.repo';
@@ -28,6 +29,7 @@ export class AdminService {
2829
private readonly prisma: PrismaService,
2930
private readonly taxFormRepo: TaxFormRepository,
3031
private readonly paymentMethodRepo: PaymentMethodRepository,
32+
private readonly paymentsService: PaymentsService,
3133
) {}
3234

3335
private getPaymentsByWinningsId(winningsId: string, paymentId?: string) {
@@ -265,9 +267,7 @@ export class AdminService {
265267
});
266268

267269
if (winning?.winner_id) {
268-
await this.reconcileWinningsStatusOnUserDetailsUpdate(
269-
winning.winner_id,
270-
);
270+
await this.paymentsService.reconcileUserPayments(winning.winner_id);
271271
}
272272
}
273273

@@ -448,49 +448,6 @@ export class AdminService {
448448
});
449449
}
450450

451-
/**
452-
* Update payment for user from one status to another
453-
*
454-
* @param userId user id
455-
* @param fromStatus from status
456-
* @param toStatus to status
457-
* @param tx transaction
458-
*/
459-
updateWinningsStatus(userId, fromStatus, toStatus) {
460-
return this.prisma.$executeRaw`
461-
UPDATE payment
462-
SET payment_status = ${toStatus}::payment_status,
463-
updated_at = now(),
464-
updated_by = 'system',
465-
version = version + 1
466-
FROM winnings
467-
WHERE payment.winnings_id = winnings.winning_id
468-
AND winnings.winner_id = ${userId}
469-
AND payment.payment_status = ${fromStatus}::payment_status AND version = version
470-
`;
471-
}
472-
473-
/**
474-
* Reconcile winning if user data updated
475-
*
476-
* @param userId user id
477-
*/
478-
async reconcileWinningsStatusOnUserDetailsUpdate(userId) {
479-
const hasTaxForm = await this.taxFormRepo.hasActiveTaxForm(userId);
480-
const hasPaymentMethod =
481-
await this.paymentMethodRepo.hasVerifiedPaymentMethod(userId);
482-
let fromStatus, toStatus;
483-
if (hasTaxForm && hasPaymentMethod) {
484-
fromStatus = PaymentStatus.ON_HOLD;
485-
toStatus = PaymentStatus.OWED;
486-
} else {
487-
fromStatus = PaymentStatus.OWED;
488-
toStatus = PaymentStatus.ON_HOLD;
489-
}
490-
491-
await this.updateWinningsStatus(userId, fromStatus, toStatus);
492-
}
493-
494451
/**
495452
* Get winning audit for winningId
496453
* @param winningId the winningId

src/api/repository/winnings.repo.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export class WinningsRepository {
129129
CASE WHEN utx.tax_form_status = 'ACTIVE' THEN TRUE ELSE FALSE END as "taxFormSetupComplete",
130130
CASE WHEN upm.status = 'CONNECTED' THEN TRUE ELSE FALSE END as "payoutSetupComplete"
131131
FROM user_payment_methods upm
132-
LEFT JOIN user_tax_form_associations utx ON upm.user_id = utx.user_id
132+
LEFT JOIN user_tax_form_associations utx ON upm.user_id = utx.user_id AND utx.tax_form_status = 'ACTIVE'
133133
WHERE upm.user_id IN (${Prisma.join(uniq(winnings.map((w) => w.winner_id)))})
134134
`;
135135

src/api/webhooks/trolley/handlers/recipient-account.handler.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ import {
77
RecipientAccountWebhookEvent,
88
} from './recipient-account.types';
99
import { payment_method_status } from '@prisma/client';
10+
import { PaymentsService } from 'src/shared/payments';
1011

1112
@Injectable()
1213
export class RecipientAccountHandler {
13-
constructor(private readonly prisma: PrismaService) {}
14+
constructor(
15+
private readonly prisma: PrismaService,
16+
private readonly paymentsService: PaymentsService,
17+
) {}
1418

1519
/**
1620
* Updates the status of the related trolley user_payment_method based on the presence of primary
@@ -126,6 +130,8 @@ export class RecipientAccountHandler {
126130
}
127131

128132
await this.updateUserPaymentMethod(payload.recipientId);
133+
134+
await this.paymentsService.reconcileUserPayments(recipient.user_id);
129135
}
130136

131137
/**
@@ -162,5 +168,9 @@ export class RecipientAccountHandler {
162168
await this.updateUserPaymentMethod(
163169
recipientPaymentMethod.trolley_recipient.trolley_id,
164170
);
171+
172+
await this.paymentsService.reconcileUserPayments(
173+
recipientPaymentMethod.trolley_recipient.user_id,
174+
);
165175
}
166176
}

src/api/webhooks/trolley/handlers/tax-form.handler.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
22
import { WebhookEvent } from '../../webhooks.decorators';
33
import { PrismaService } from 'src/shared/global/prisma.service';
44
import { tax_form_status, trolley_recipient } from '@prisma/client';
5+
import { PaymentsService } from 'src/shared/payments';
56
import {
67
TrolleyTaxFormStatus,
78
TaxFormStatusUpdatedEvent,
@@ -11,7 +12,10 @@ import {
1112

1213
@Injectable()
1314
export class TaxFormHandler {
14-
constructor(private readonly prisma: PrismaService) {}
15+
constructor(
16+
private readonly prisma: PrismaService,
17+
private readonly paymentsService: PaymentsService,
18+
) {}
1519

1620
getDbRecipientById(id: string) {
1721
return this.prisma.trolley_recipient.findUnique({
@@ -34,7 +38,6 @@ export class TaxFormHandler {
3438
where: {
3539
user_id: recipient.user_id,
3640
tax_form_id: taxFormId,
37-
tax_form_status: tax_form_status.ACTIVE,
3841
},
3942
});
4043

@@ -43,9 +46,10 @@ export class TaxFormHandler {
4346
taxFormData.status === TrolleyTaxFormStatus.Voided &&
4447
existingFormAssociation
4548
) {
46-
return this.prisma.user_tax_form_associations.delete({
49+
return this.prisma.user_tax_form_associations.deleteMany({
4750
where: {
48-
id: existingFormAssociation.id,
51+
user_id: recipient.user_id,
52+
tax_form_id: taxFormId,
4953
},
5054
});
5155
}
@@ -102,5 +106,7 @@ export class TaxFormHandler {
102106
recipient,
103107
taxFormData,
104108
);
109+
110+
await this.paymentsService.reconcileUserPayments(recipient.user_id);
105111
}
106112
}

src/api/webhooks/trolley/trolley.service.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ if (!trolleyWhHmac) {
2222
export class TrolleyService {
2323
constructor(
2424
@Inject('trolleyHandlerFns')
25-
private readonly handlers,
25+
private readonly handlers: Map<
26+
string,
27+
(eventPayload: any) => Promise<unknown>
28+
>,
2629
private readonly prisma: PrismaService,
2730
) {}
2831

src/api/webhooks/webhooks.module.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
22
import { WebhooksController } from './webhooks.controller';
33
import { TrolleyService } from './trolley/trolley.service';
44
import { TrolleyWebhookHandlers } from './trolley/handlers';
5+
import { PaymentsModule } from 'src/shared/payments';
56

67
@Module({
7-
imports: [],
8+
imports: [PaymentsModule],
89
controllers: [WebhooksController],
910
providers: [...TrolleyWebhookHandlers, TrolleyService],
1011
})

src/shared/payments/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './payments.module';
2+
export * from './payments.service';
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Module } from '@nestjs/common';
2+
import { PaymentsService } from './payments.service';
3+
4+
@Module({
5+
providers: [PaymentsService],
6+
exports: [PaymentsService],
7+
})
8+
export class PaymentsModule {}
+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { PrismaService } from '../global/prisma.service';
3+
import { payment_status, Prisma } from '@prisma/client';
4+
import { uniq } from 'lodash';
5+
6+
@Injectable()
7+
export class PaymentsService {
8+
constructor(private readonly prisma: PrismaService) {}
9+
10+
/**
11+
* Retrieves the payout setup status for a list of users.
12+
*
13+
* This method queries the database to determine whether each user's payout setup
14+
* is complete or still in progress. A user's payout setup is considered complete
15+
* if their tax form status is 'ACTIVE' and their payment method status is 'CONNECTED'.
16+
*
17+
* @param userIds - An array of user IDs for which to retrieve the payout setup status.
18+
* @returns A promise that resolves to an object containing two arrays:
19+
* - `complete`: An array of user IDs whose payout setup is complete.
20+
* - `inProgress`: An array of user IDs whose payout setup is still in progress.
21+
*
22+
* @throws Will throw an error if the database query fails.
23+
*/
24+
private async getUsersPayoutStatus(userIds: string[]) {
25+
const usersPayoutStatus = await this.prisma.$queryRaw<
26+
{
27+
userId: string;
28+
setupComplete: boolean;
29+
}[]
30+
>`
31+
SELECT
32+
upm.user_id as "userId",
33+
CASE WHEN utx.tax_form_status = 'ACTIVE' AND upm.status = 'CONNECTED' THEN TRUE ELSE FALSE END as "setupComplete"
34+
FROM user_payment_methods upm
35+
LEFT JOIN user_tax_form_associations utx ON upm.user_id = utx.user_id AND utx.tax_form_status = 'ACTIVE'
36+
WHERE upm.user_id IN (${Prisma.join(uniq(userIds))})
37+
`;
38+
39+
const setupStatusMap = {
40+
complete: [] as string[],
41+
inProgress: [] as string[],
42+
};
43+
44+
usersPayoutStatus.forEach((user) => {
45+
setupStatusMap[user.setupComplete ? 'complete' : 'inProgress'].push(
46+
user.userId,
47+
);
48+
});
49+
50+
return setupStatusMap;
51+
}
52+
53+
/**
54+
* Updates the payment status of users' payments based on the provided user IDs and the desired status.
55+
*
56+
* @param userIds - An array of user IDs whose payment statuses need to be updated.
57+
* @param setOnHold - A boolean indicating whether to set the payment status to "ON_HOLD" (true)
58+
* or revert it to "OWED" (false).
59+
* @returns A raw Prisma query that updates the payment statuses in the database.
60+
*
61+
* The function performs the following:
62+
* - Updates the `payment_status` field in the `payment` table.
63+
* - Changes the status to "ON_HOLD" if `setOnHold` is true, or to "OWED" if `setOnHold` is false.
64+
* - Ensures that only payments associated with the specified users' winnings are updated.
65+
*/
66+
private updateUserPaymentsStatus(userIds: string[], setOnHold: boolean) {
67+
return this.prisma.$queryRaw`
68+
UPDATE payment
69+
SET payment_status = ${setOnHold ? payment_status.ON_HOLD : payment_status.OWED}::payment_status
70+
FROM winnings w
71+
WHERE payment.payment_status = ${setOnHold ? payment_status.OWED : payment_status.ON_HOLD}::payment_status
72+
AND payment.winnings_id = w.winning_id
73+
AND w.winner_id IN (${Prisma.join(uniq(userIds))});
74+
`;
75+
}
76+
77+
/**
78+
* Reconciles the payment statuses of the specified users by updating their payment records.
79+
*
80+
* @param userIds - A list of user IDs whose payment statuses need to be reconciled.
81+
*
82+
* The method performs the following steps:
83+
* 1. Retrieves the payout status of the specified users.
84+
* 2. Updates the payment status for users whose payments are complete to `OWED`.
85+
* 3. Updates the payment status for users whose payments are in progress to `ON_HOLD`.
86+
*
87+
* This ensures that the payment statuses are accurately reflected in the system.
88+
*/
89+
async reconcileUserPayments(...userIds: string[]) {
90+
const usersPayoutStatus = await this.getUsersPayoutStatus(userIds);
91+
92+
if (usersPayoutStatus.complete.length) {
93+
await this.updateUserPaymentsStatus(usersPayoutStatus.complete, false);
94+
}
95+
96+
if (usersPayoutStatus.inProgress.length) {
97+
await this.updateUserPaymentsStatus(usersPayoutStatus.inProgress, true);
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)