Skip to content

Commit 983158e

Browse files
authored
Merge pull request #71 from topcoder-platform/PM-1345_trolley-trust-module
Pm 1345 trolley trust module
2 parents b199ca5 + c318b0a commit 983158e

File tree

17 files changed

+250
-20
lines changed

17 files changed

+250
-20
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- CreateEnum
2+
CREATE TYPE "verification_status" AS ENUM ('ACTIVE', 'INACTIVE');
3+
4+
-- CreateTable
5+
CREATE TABLE "user_identity_verification_associations" (
6+
"id" UUID NOT NULL DEFAULT uuid_generate_v4(),
7+
"user_id" VARCHAR(80) NOT NULL,
8+
"verification_id" TEXT NOT NULL,
9+
"date_filed" TIMESTAMP(6) WITHOUT TIME ZONE NOT NULL,
10+
"verification_status" "verification_status" NOT NULL,
11+
12+
CONSTRAINT "user_identity_verification_associations_pkey" PRIMARY KEY ("id")
13+
);

prisma/schema.prisma

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,13 +207,26 @@ model trolley_webhook_log {
207207
created_at DateTime? @default(now()) @db.Timestamp(6)
208208
}
209209

210+
model user_identity_verification_associations {
211+
id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
212+
user_id String @db.VarChar(80)
213+
verification_id String @db.Text
214+
date_filed DateTime @db.Timestamp(6)
215+
verification_status verification_status
216+
}
217+
210218
model trolley_recipient_payment_method {
211219
id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
212220
trolley_recipient_id Int
213221
recipient_account_id String @unique @db.VarChar(80)
214222
trolley_recipient trolley_recipient @relation(fields: [trolley_recipient_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "fk_trolley_recipient_trolley_recipient_payment_method")
215223
}
216224

225+
enum verification_status {
226+
ACTIVE
227+
INACTIVE
228+
}
229+
217230
enum action_type {
218231
INITIATE_WITHDRAWAL
219232
ADD_WITHDRAWAL_METHOD
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { verification_status } from '@prisma/client';
3+
import { PrismaService } from 'src/shared/global/prisma.service';
4+
5+
@Injectable()
6+
export class IdentityVerificationRepository {
7+
constructor(private readonly prisma: PrismaService) {}
8+
9+
/**
10+
* Checks if the user has completed their identity verification by checking the identity verification associations
11+
*
12+
* @param userId - The unique identifier of the user.
13+
* @returns A promise that resolves to `true` if the user has at least one active identity verification association, otherwise `false`.
14+
*/
15+
async completedIdentityVerification(userId: string): Promise<boolean> {
16+
const count =
17+
await this.prisma.user_identity_verification_associations.count({
18+
where: {
19+
user_id: userId,
20+
verification_status: verification_status.ACTIVE,
21+
},
22+
});
23+
24+
return count > 0;
25+
}
26+
}

src/api/user/user.controller.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,19 @@ import { ResponseDto, ResponseStatusType } from 'src/dto/api-response.dto';
2323
import { SearchWinningResult, WinningRequestDto } from 'src/dto/winning.dto';
2424
import { UserInfo } from 'src/dto/user.type';
2525
import { UserWinningRequestDto } from './dto/user.dto';
26+
import { PaymentsService } from 'src/shared/payments';
27+
import { Logger } from 'src/shared/global';
2628

2729
@ApiTags('UserWinning')
2830
@Controller('/user')
2931
@ApiBearerAuth()
3032
export class UserController {
31-
constructor(private readonly winningsRepo: WinningsRepository) {}
33+
private readonly logger = new Logger(UserController.name);
34+
35+
constructor(
36+
private readonly winningsRepo: WinningsRepository,
37+
private readonly paymentsService: PaymentsService,
38+
) {}
3239

3340
@Post('/winnings')
3441
@Roles(Role.User)
@@ -59,6 +66,20 @@ export class UserController {
5966
throw new ForbiddenException('insufficient permissions');
6067
}
6168

69+
try {
70+
await this.paymentsService.reconcileUserPayments(user.id);
71+
} catch (e) {
72+
this.logger.error('Error reconciling user payments', e);
73+
74+
return {
75+
error: {
76+
code: HttpStatus.INTERNAL_SERVER_ERROR,
77+
message: 'Failed to reconcile user payments.',
78+
},
79+
status: ResponseStatusType.ERROR,
80+
} as ResponseDto<SearchWinningResult>;
81+
}
82+
6283
const result = await this.winningsRepo.searchWinnings(
6384
body as WinningRequestDto,
6485
);

src/api/user/user.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Module } from '@nestjs/common';
22
import { UserController } from './user.controller';
33
import { WinningsRepository } from '../repository/winnings.repo';
4+
import { PaymentsModule } from 'src/shared/payments';
45

56
@Module({
6-
imports: [],
7+
imports: [PaymentsModule],
78
controllers: [UserController],
89
providers: [WinningsRepository],
910
})

src/api/wallet/wallet.module.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@ import { WalletController } from './wallet.controller';
33
import { WalletService } from './wallet.service';
44
import { TaxFormRepository } from '../repository/taxForm.repo';
55
import { PaymentMethodRepository } from '../repository/paymentMethod.repo';
6+
import { IdentityVerificationRepository } from '../repository/identity-verification.repo';
67

78
@Module({
89
imports: [],
910
controllers: [WalletController],
10-
providers: [WalletService, TaxFormRepository, PaymentMethodRepository],
11+
providers: [
12+
WalletService,
13+
TaxFormRepository,
14+
PaymentMethodRepository,
15+
IdentityVerificationRepository,
16+
],
1117
})
1218
export class WalletModule {}

src/api/wallet/wallet.service.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
TrolleyService,
1313
} from 'src/shared/global/trolley.service';
1414
import { Logger } from 'src/shared/global';
15+
import { IdentityVerificationRepository } from '../repository/identity-verification.repo';
1516

1617
/**
1718
* The winning service.
@@ -28,6 +29,7 @@ export class WalletService {
2829
private readonly prisma: PrismaService,
2930
private readonly taxFormRepo: TaxFormRepository,
3031
private readonly paymentMethodRepo: PaymentMethodRepository,
32+
private readonly identityVerificationRepo: IdentityVerificationRepository,
3133
private readonly trolleyService: TrolleyService,
3234
) {}
3335

@@ -57,6 +59,10 @@ export class WalletService {
5759
const winnings = await this.getWinningsTotalsByWinnerID(userId);
5860

5961
const hasActiveTaxForm = await this.taxFormRepo.hasActiveTaxForm(userId);
62+
const isIdentityVerified =
63+
await this.identityVerificationRepo.completedIdentityVerification(
64+
userId,
65+
);
6066
const hasVerifiedPaymentMethod = Boolean(
6167
await this.paymentMethodRepo.getConnectedPaymentMethod(userId),
6268
);
@@ -91,6 +97,9 @@ export class WalletService {
9197
taxForm: {
9298
isSetupComplete: hasActiveTaxForm,
9399
},
100+
identityVerification: {
101+
isSetupComplete: isIdentityVerified,
102+
},
94103
...(taxWithholdingDetails ?? {}),
95104
};
96105
} catch (error) {

src/api/webhooks/trolley/handlers/index.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { PaymentHandler } from './payment.handler';
33
import { TaxFormHandler } from './tax-form.handler';
44
import { getWebhooksEventHandlersProvider } from '../../webhooks.event-handlers.provider';
55
import { RecipientAccountHandler } from './recipient-account.handler';
6+
import { RecipientVerificationHandler } from './recipient-verification.handler';
67

78
export const TrolleyWebhookHandlers: Provider[] = [
89
getWebhooksEventHandlersProvider(
@@ -12,14 +13,26 @@ export const TrolleyWebhookHandlers: Provider[] = [
1213

1314
PaymentHandler,
1415
RecipientAccountHandler,
16+
RecipientVerificationHandler,
1517
TaxFormHandler,
1618
{
1719
provide: 'TrolleyWebhookHandlers',
18-
inject: [PaymentHandler, RecipientAccountHandler, TaxFormHandler],
20+
inject: [
21+
PaymentHandler,
22+
RecipientAccountHandler,
23+
RecipientVerificationHandler,
24+
TaxFormHandler,
25+
],
1926
useFactory: (
2027
paymentHandler: PaymentHandler,
2128
recipientAccountHandler: RecipientAccountHandler,
29+
recipientVerificationHandler: RecipientVerificationHandler,
2230
taxFormHandler: TaxFormHandler,
23-
) => [paymentHandler, recipientAccountHandler, taxFormHandler],
31+
) => [
32+
paymentHandler,
33+
recipientAccountHandler,
34+
recipientVerificationHandler,
35+
taxFormHandler,
36+
],
2437
},
2538
];
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { WebhookEvent } from '../../webhooks.decorators';
3+
import { PrismaService } from 'src/shared/global/prisma.service';
4+
import { PaymentsService } from 'src/shared/payments';
5+
import { Logger } from 'src/shared/global';
6+
import {
7+
RecipientVerificationStatusUpdateEventData,
8+
RecipientVerificationWebhookEvent,
9+
} from './recipient-verification.types';
10+
import { Prisma, verification_status } from '@prisma/client';
11+
12+
@Injectable()
13+
export class RecipientVerificationHandler {
14+
private readonly logger = new Logger(RecipientVerificationHandler.name);
15+
16+
constructor(
17+
private readonly prisma: PrismaService,
18+
private readonly paymentsService: PaymentsService,
19+
) {}
20+
21+
@WebhookEvent(RecipientVerificationWebhookEvent.statusUpdated)
22+
async handleStatusUpdated(
23+
payload: RecipientVerificationStatusUpdateEventData,
24+
): Promise<void> {
25+
const recipient = await this.prisma.trolley_recipient.findFirst({
26+
where: { trolley_id: payload.recipientId },
27+
});
28+
29+
if (!recipient) {
30+
this.logger.error(
31+
`Recipient with trolley_id ${payload.recipientId} not found.`,
32+
);
33+
throw new Error(
34+
`Recipient with trolley_id ${payload.recipientId} not found.`,
35+
);
36+
}
37+
38+
const userIDV =
39+
await this.prisma.user_identity_verification_associations.findFirst({
40+
where: {
41+
user_id: recipient.user_id,
42+
},
43+
});
44+
45+
const verificationData: Prisma.user_identity_verification_associationsCreateInput =
46+
{
47+
user_id: recipient.user_id,
48+
verification_id: payload.id,
49+
date_filed: payload.submittedAt ?? new Date(),
50+
verification_status:
51+
payload.status === 'approved'
52+
? verification_status.ACTIVE
53+
: verification_status.INACTIVE,
54+
};
55+
56+
if (userIDV) {
57+
await this.prisma.user_identity_verification_associations.update({
58+
where: {
59+
id: userIDV.id,
60+
},
61+
data: { ...verificationData },
62+
});
63+
} else {
64+
await this.prisma.user_identity_verification_associations.create({
65+
data: { ...verificationData },
66+
});
67+
}
68+
69+
await this.paymentsService.reconcileUserPayments(recipient.user_id);
70+
}
71+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export enum RecipientVerificationWebhookEvent {
2+
statusUpdated = 'recipientVerification.status_updated',
3+
}
4+
5+
export interface RecipientVerificationStatusUpdateEventData {
6+
type: string;
7+
recipientId: string;
8+
status: 'pending' | 'approved' | 'rejected';
9+
createdAt: string;
10+
updatedAt: string;
11+
submittedAt: string | null;
12+
decisionAt: string | null;
13+
id: string;
14+
reasonType: string | null;
15+
verifiedData: {
16+
channel: 'sms' | 'email';
17+
phone: string;
18+
phoneExtension: string | null;
19+
country: string;
20+
};
21+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export class TrolleyService {
142142
this.logger.debug(
143143
`Invoking handler for event - ${requestId} - ${model}.${action}`,
144144
);
145-
await handler(body[model]);
145+
await handler(body[model] ?? Object.values(body)?.[0]);
146146

147147
this.logger.debug(`Successfully processed event with ID: ${requestId}`);
148148
await this.setEventState(requestId, webhook_status.processed);

src/api/winnings/winnings.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { WinningsRepository } from '../repository/winnings.repo';
66
import { TaxFormRepository } from '../repository/taxForm.repo';
77
import { PaymentMethodRepository } from '../repository/paymentMethod.repo';
88
import { TopcoderModule } from 'src/shared/topcoder/topcoder.module';
9+
import { IdentityVerificationRepository } from '../repository/identity-verification.repo';
910

1011
@Module({
1112
imports: [TopcoderModule],
@@ -16,6 +17,7 @@ import { TopcoderModule } from 'src/shared/topcoder/topcoder.module';
1617
TaxFormRepository,
1718
WinningsRepository,
1819
PaymentMethodRepository,
20+
IdentityVerificationRepository,
1921
],
2022
})
2123
export class WinningsModule {}

src/api/winnings/winnings.service.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { BASIC_MEMBER_FIELDS } from 'src/shared/topcoder';
1919
import { ENV_CONFIG } from 'src/config';
2020
import { Logger } from 'src/shared/global';
2121
import { TopcoderEmailService } from 'src/shared/topcoder/tc-email.service';
22+
import { IdentityVerificationRepository } from '../repository/identity-verification.repo';
2223

2324
/**
2425
* The winning service.
@@ -37,6 +38,7 @@ export class WinningsService {
3738
private readonly paymentMethodRepo: PaymentMethodRepository,
3839
private readonly originRepo: OriginRepository,
3940
private readonly tcMembersService: TopcoderMembersService,
41+
private readonly identityVerificationRepo: IdentityVerificationRepository,
4042
private readonly tcEmailService: TopcoderEmailService,
4143
) {}
4244

@@ -182,6 +184,10 @@ export class WinningsService {
182184
const hasConnectedPaymentMethod = Boolean(
183185
await this.paymentMethodRepo.getConnectedPaymentMethod(body.winnerId),
184186
);
187+
const isIdentityVerified =
188+
await this.identityVerificationRepo.completedIdentityVerification(
189+
userId,
190+
);
185191

186192
for (const detail of body.details || []) {
187193
const paymentModel = {
@@ -198,7 +204,7 @@ export class WinningsService {
198204

199205
paymentModel.net_amount = Prisma.Decimal(detail.grossAmount);
200206
paymentModel.payment_status =
201-
hasConnectedPaymentMethod && hasActiveTaxForm
207+
hasConnectedPaymentMethod && hasActiveTaxForm && isIdentityVerified
202208
? PaymentStatus.OWED
203209
: PaymentStatus.ON_HOLD;
204210

src/api/withdrawal/withdrawal.module.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@ import { WithdrawalService } from './withdrawal.service';
55
import { TaxFormRepository } from '../repository/taxForm.repo';
66
import { PaymentMethodRepository } from '../repository/paymentMethod.repo';
77
import { TopcoderModule } from 'src/shared/topcoder/topcoder.module';
8+
import { IdentityVerificationRepository } from '../repository/identity-verification.repo';
89

910
@Module({
1011
imports: [PaymentsModule, TopcoderModule],
1112
controllers: [WithdrawalController],
12-
providers: [WithdrawalService, TaxFormRepository, PaymentMethodRepository],
13+
providers: [
14+
WithdrawalService,
15+
TaxFormRepository,
16+
PaymentMethodRepository,
17+
IdentityVerificationRepository,
18+
],
1319
})
1420
export class WithdrawalModule {}

0 commit comments

Comments
 (0)