From c6d99d73911a3e1a9dbf910fe79496b5f4115faf Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 16 May 2025 11:06:18 +0300 Subject: [PATCH 1/3] PM-1237 - add support for withdraw memo --- src/api/withdrawal/dto/withdraw.dto.ts | 37 +++++++++++++++++++-- src/api/withdrawal/withdrawal.controller.ts | 2 ++ src/api/withdrawal/withdrawal.service.ts | 8 ++++- src/config/config.env.ts | 12 ++++++- src/shared/global/trolley.service.ts | 3 +- 5 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/api/withdrawal/dto/withdraw.dto.ts b/src/api/withdrawal/dto/withdraw.dto.ts index 5073a11..a8259cc 100644 --- a/src/api/withdrawal/dto/withdraw.dto.ts +++ b/src/api/withdrawal/dto/withdraw.dto.ts @@ -1,14 +1,45 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ArrayNotEmpty, IsArray, IsNotEmpty, IsUUID } from 'class-validator'; +import { + ArrayNotEmpty, + IsArray, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, + MaxLength, +} from 'class-validator'; +import { ENV_CONFIG } from 'src/config'; -export class WithdrawRequestDto { +export function ConditionalApiProperty( + condition: boolean, + options: Parameters[0], +) { + return condition ? ApiProperty(options) : () => undefined; +} +export class WithdrawRequestDtoBase { @ApiProperty({ description: 'The ID of the winnings to withdraw', example: ['3fa85f64-5717-4562-b3fc-2c963f66afa6'], }) @IsArray() @ArrayNotEmpty() - @IsUUID('4',{ each: true }) + @IsUUID('4', { each: true }) @IsNotEmpty({ each: true }) winningsIds: string[]; } + +export class WithdrawRequestDtoWithMemo extends WithdrawRequestDtoBase { + @ApiProperty({ + description: + 'A short note (30 chars max) which that will show up on your bank statement', + example: 'Topcoder payment for week 05/17', + }) + @IsString() + @IsOptional() + @MaxLength(30) + memo?: string; +} + +export const WithdrawRequestDto = ENV_CONFIG.ACCEPT_CUSTOM_PAYMENTS_MEMO + ? WithdrawRequestDtoWithMemo + : WithdrawRequestDtoBase; diff --git a/src/api/withdrawal/withdrawal.controller.ts b/src/api/withdrawal/withdrawal.controller.ts index 8f860f5..b6759be 100644 --- a/src/api/withdrawal/withdrawal.controller.ts +++ b/src/api/withdrawal/withdrawal.controller.ts @@ -46,6 +46,7 @@ export class WithdrawalController { @HttpCode(HttpStatus.OK) async doWithdraw( @User() user: UserInfo, + // @ts-expect-error: Suppress error for 'WithdrawRequestDto' being used as a type @Body() body: WithdrawRequestDto, ): Promise> { const result = new ResponseDto(); @@ -55,6 +56,7 @@ export class WithdrawalController { user.id, user.handle, body.winningsIds, + body.memo, ); result.status = ResponseStatusType.SUCCESS; return result; diff --git a/src/api/withdrawal/withdrawal.service.ts b/src/api/withdrawal/withdrawal.service.ts index 68bd584..4117969 100644 --- a/src/api/withdrawal/withdrawal.service.ts +++ b/src/api/withdrawal/withdrawal.service.ts @@ -151,7 +151,12 @@ export class WithdrawalService { } } - async withdraw(userId: string, userHandle: string, winningsIds: string[]) { + async withdraw( + userId: string, + userHandle: string, + winningsIds: string[], + paymentMemo?: string, + ) { this.logger.log('Processing withdrawal request'); const hasActiveTaxForm = await this.taxFormRepo.hasActiveTaxForm(userId); @@ -206,6 +211,7 @@ export class WithdrawalService { paymentBatch.id, totalAmount, paymentRelease.payment_release_id, + paymentMemo, ); await this.updateDbReleaseRecord(tx, paymentRelease, trolleyPayment.id); diff --git a/src/config/config.env.ts b/src/config/config.env.ts index cfbb9db..28adf05 100644 --- a/src/config/config.env.ts +++ b/src/config/config.env.ts @@ -1,4 +1,5 @@ -import { IsInt, IsOptional, IsString } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { IsBoolean, IsInt, IsOptional, IsString } from 'class-validator'; export class ConfigEnv { @IsString() @@ -54,4 +55,13 @@ export class ConfigEnv { @IsInt() @IsOptional() TROLLEY_MINIMUM_PAYMENT_AMOUNT: number = 0; + + @IsBoolean() + @IsOptional() + @Transform(({ value }) => { + if (typeof value === 'boolean') return value; + + return value.toLowerCase() === 'true'; + }) + ACCEPT_CUSTOM_PAYMENTS_MEMO; } diff --git a/src/shared/global/trolley.service.ts b/src/shared/global/trolley.service.ts index e17a1b9..ccdfe78 100644 --- a/src/shared/global/trolley.service.ts +++ b/src/shared/global/trolley.service.ts @@ -110,6 +110,7 @@ export class TrolleyService { paymentBatchId: string, totalAmount: number, transactionId: string, + paymentMemo?: string, ) { const paymentPayload = { recipient: { @@ -117,7 +118,7 @@ export class TrolleyService { }, sourceAmount: totalAmount.toString(), sourceCurrency: 'USD', - memo: 'Topcoder payment', + memo: paymentMemo ?? 'Topcoder payment', externalId: transactionId, }; From 272b90a2ef1a929aa808a7e973fe779d4dbae145 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 16 May 2025 11:09:07 +0300 Subject: [PATCH 2/3] remove code --- src/api/withdrawal/dto/withdraw.dto.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/api/withdrawal/dto/withdraw.dto.ts b/src/api/withdrawal/dto/withdraw.dto.ts index a8259cc..4099c85 100644 --- a/src/api/withdrawal/dto/withdraw.dto.ts +++ b/src/api/withdrawal/dto/withdraw.dto.ts @@ -10,12 +10,6 @@ import { } from 'class-validator'; import { ENV_CONFIG } from 'src/config'; -export function ConditionalApiProperty( - condition: boolean, - options: Parameters[0], -) { - return condition ? ApiProperty(options) : () => undefined; -} export class WithdrawRequestDtoBase { @ApiProperty({ description: 'The ID of the winnings to withdraw', From 02d96a4cd005925f9f9b20a5cba7c652d0ffb6c0 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 16 May 2025 11:13:38 +0300 Subject: [PATCH 3/3] check env var type --- src/config/config.env.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/config/config.env.ts b/src/config/config.env.ts index 28adf05..222c4ed 100644 --- a/src/config/config.env.ts +++ b/src/config/config.env.ts @@ -61,7 +61,11 @@ export class ConfigEnv { @Transform(({ value }) => { if (typeof value === 'boolean') return value; - return value.toLowerCase() === 'true'; + if (typeof value === 'string') { + return value.toLowerCase() === 'true'; + } + + return false; }) ACCEPT_CUSTOM_PAYMENTS_MEMO; }