diff --git a/src/api/withdrawal/dto/withdraw.dto.ts b/src/api/withdrawal/dto/withdraw.dto.ts index 5073a11..4099c85 100644 --- a/src/api/withdrawal/dto/withdraw.dto.ts +++ b/src/api/withdrawal/dto/withdraw.dto.ts @@ -1,14 +1,39 @@ 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 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..222c4ed 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,17 @@ export class ConfigEnv { @IsInt() @IsOptional() TROLLEY_MINIMUM_PAYMENT_AMOUNT: number = 0; + + @IsBoolean() + @IsOptional() + @Transform(({ value }) => { + if (typeof value === 'boolean') return value; + + if (typeof value === 'string') { + return value.toLowerCase() === 'true'; + } + + return false; + }) + 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, };