Skip to content

Commit 2b1170b

Browse files
committed
PM-1099 - implement withdraw payments
1 parent b1bd256 commit 2b1170b

File tree

12 files changed

+468
-18
lines changed

12 files changed

+468
-18
lines changed

src/api/api.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { AdminModule } from './admin/admin.module';
1515
import { WinningsModule } from './winnings/winnings.module';
1616
import { UserModule } from './user/user.module';
1717
import { WalletModule } from './wallet/wallet.module';
18+
import { WithdrawalModule } from './withdrawal/withdrawal.module';
1819

1920
@Module({
2021
imports: [
@@ -26,6 +27,7 @@ import { WalletModule } from './wallet/wallet.module';
2627
WinningsModule,
2728
UserModule,
2829
WalletModule,
30+
WithdrawalModule,
2931
],
3032
controllers: [HealthCheckController],
3133
providers: [

src/api/repository/paymentMethod.repo.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Injectable } from '@nestjs/common';
2-
import { payment_method_status } from '@prisma/client';
2+
import { payment_method_status, user_payment_methods } from '@prisma/client';
33
import { PrismaService } from 'src/shared/global/prisma.service';
44

55
@Injectable()
@@ -12,7 +12,9 @@ export class PaymentMethodRepository {
1212
* @param userId user id
1313
* @param tx transaction
1414
*/
15-
async hasVerifiedPaymentMethod(userId: string): Promise<boolean> {
15+
async hasVerifiedPaymentMethod(
16+
userId: string,
17+
): Promise<user_payment_methods | null> {
1618
const connectedUserPaymentMethod =
1719
await this.prisma.user_payment_methods.findFirst({
1820
where: {
@@ -21,6 +23,6 @@ export class PaymentMethodRepository {
2123
},
2224
});
2325

24-
return !!connectedUserPaymentMethod;
26+
return connectedUserPaymentMethod;
2527
}
2628
}
Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,63 @@
11
import { Injectable } from '@nestjs/common';
2-
// import { WebhookEvent } from '../../webhooks.decorators';
2+
import {
3+
PaymentProcessedEventData,
4+
PaymentWebhookEvent,
5+
} from './payment.types';
6+
import { WebhookEvent } from '../../webhooks.decorators';
7+
import { PaymentsService } from 'src/shared/payments';
8+
import { payment_status } from '@prisma/client';
9+
import { PrismaService } from 'src/shared/global/prisma.service';
310

411
@Injectable()
512
export class PaymentHandler {
6-
// @WebhookEvent(TrolleyWebhookEvent.paymentCreated)
7-
// async handlePaymentCreated(payload: any): Promise<any> {
8-
// // TODO: Build out logic for payment.created event
9-
// console.log('handling', TrolleyWebhookEvent.paymentCreated);
10-
// }
13+
constructor(
14+
private readonly prisma: PrismaService,
15+
private readonly paymentsService: PaymentsService,
16+
) {}
17+
18+
@WebhookEvent(
19+
PaymentWebhookEvent.processed,
20+
PaymentWebhookEvent.failed,
21+
PaymentWebhookEvent.returned,
22+
)
23+
async handlePaymentProcessed(
24+
payload: PaymentProcessedEventData,
25+
): Promise<any> {
26+
// TODO: remove slice-1
27+
const winningIds = (payload.externalId ?? '').split(',').slice(0, -1);
28+
29+
if (!winningIds.length) {
30+
console.error(
31+
`No matching payments in our db to process for incoming payment.processed with memo '${payload.memo}'.`,
32+
);
33+
}
34+
35+
if (payload.status !== 'processed') {
36+
await this.prisma.$transaction(async (tx) => {
37+
await this.paymentsService.updatePaymentProcessingState(
38+
winningIds,
39+
payment_status.PROCESSING,
40+
tx,
41+
);
42+
43+
await this.paymentsService.updatePaymentReleaseState(
44+
payload.id,
45+
'FAILED',
46+
);
47+
});
48+
}
49+
50+
await this.prisma.$transaction(async (tx) => {
51+
await this.paymentsService.updatePaymentProcessingState(
52+
winningIds,
53+
payment_status.PAID,
54+
tx,
55+
);
56+
57+
await this.paymentsService.updatePaymentReleaseState(
58+
payload.id,
59+
'PROCESSED',
60+
);
61+
});
62+
}
1163
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export enum PaymentWebhookEvent {
2+
processed = 'payment.processed',
3+
failed = 'payment.failed',
4+
returned = 'payment.returned',
5+
}
6+
7+
export interface PaymentProcessedEventData {
8+
id: string;
9+
recipient: {
10+
id: string;
11+
referenceId: string;
12+
email: string;
13+
};
14+
status: 'processed' | 'failed' | 'returned';
15+
externalId?: string;
16+
sourceAmount: string; // gross amount
17+
fees: string;
18+
targetAmount: string; // net amount
19+
failureMessage: string | null;
20+
memo: string | null;
21+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString } from 'class-validator';
3+
4+
export class WithdrawRequestDto {
5+
@ApiProperty({
6+
description: 'The ID of the winnings to withdraw',
7+
example: ['3fa85f64-5717-4562-b3fc-2c963f66afa6'],
8+
})
9+
@IsArray()
10+
@ArrayNotEmpty()
11+
@IsString({ each: true })
12+
@IsNotEmpty({ each: true })
13+
paymentIds: string[];
14+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
Controller,
3+
Post,
4+
HttpCode,
5+
HttpStatus,
6+
Body,
7+
BadRequestException,
8+
} from '@nestjs/common';
9+
import {
10+
ApiOperation,
11+
ApiTags,
12+
ApiResponse,
13+
ApiBearerAuth,
14+
ApiBody,
15+
} from '@nestjs/swagger';
16+
17+
import { Role } from 'src/core/auth/auth.constants';
18+
import { Roles, User } from 'src/core/auth/decorators';
19+
import { UserInfo } from 'src/dto/user.type';
20+
import { ResponseDto, ResponseStatusType } from 'src/dto/api-response.dto';
21+
22+
import { WithdrawalService } from './withdrawal.service';
23+
import { WithdrawRequestDto } from './dto/withdraw.dto';
24+
25+
@ApiTags('Withdrawal')
26+
@Controller('/withdraw')
27+
@ApiBearerAuth()
28+
export class WithdrawalController {
29+
constructor(private readonly withdrawalService: WithdrawalService) {}
30+
31+
@Post()
32+
@Roles(Role.User)
33+
@ApiOperation({
34+
summary: 'User call this operation to process withdrawal.',
35+
description: 'jwt required.',
36+
})
37+
@ApiBody({
38+
description: 'Request body',
39+
type: WithdrawRequestDto,
40+
})
41+
@ApiResponse({
42+
status: 200,
43+
description: 'Get wallet detail successfully.',
44+
type: ResponseDto<string>,
45+
})
46+
@HttpCode(HttpStatus.OK)
47+
async doWithdraw(
48+
@User() user: UserInfo,
49+
@Body() body: WithdrawRequestDto,
50+
): Promise<ResponseDto<string>> {
51+
const result = new ResponseDto<string>();
52+
53+
try {
54+
await this.withdrawalService.withdraw(
55+
user.id,
56+
user.handle,
57+
body.paymentIds,
58+
);
59+
result.status = ResponseStatusType.SUCCESS;
60+
return result;
61+
} catch (e) {
62+
throw new BadRequestException(e.message);
63+
}
64+
}
65+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Module } from '@nestjs/common';
2+
import { PaymentsModule } from 'src/shared/payments';
3+
import { WithdrawalController } from './withdrawal.controller';
4+
import { WithdrawalService } from './withdrawal.service';
5+
import { TaxFormRepository } from '../repository/taxForm.repo';
6+
import { PaymentMethodRepository } from '../repository/paymentMethod.repo';
7+
8+
@Module({
9+
imports: [PaymentsModule],
10+
controllers: [WithdrawalController],
11+
providers: [WithdrawalService, TaxFormRepository, PaymentMethodRepository],
12+
})
13+
export class WithdrawalModule {}

0 commit comments

Comments
 (0)