-
Notifications
You must be signed in to change notification settings - Fork 1
Engagements v4 #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Engagements v4 #148
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,33 @@ export class WinningsRepository { | |
|
|
||
| constructor(private readonly prisma: PrismaService) {} | ||
|
|
||
| /** | ||
| * Extracts the optional hours-worked value from winning attributes. | ||
| * @param attributes Winning attributes payload retrieved from the database. | ||
| * @returns Parsed hours-worked value, or `undefined` when absent/invalid. | ||
| */ | ||
| private getHoursWorked( | ||
| attributes: Prisma.JsonValue | null, | ||
| ): number | undefined { | ||
| if ( | ||
| !attributes || | ||
| typeof attributes !== 'object' || | ||
| Array.isArray(attributes) | ||
| ) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const value = (attributes as Record<string, unknown>).hoursWorked; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| if (value === undefined || value === null || value === '') { | ||
| return undefined; | ||
| } | ||
|
|
||
| const parsedValue = Number(value); | ||
| return Number.isFinite(parsedValue) && parsedValue > 0 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| ? parsedValue | ||
| : undefined; | ||
| } | ||
|
|
||
| private generateFilterDate(date?: DateFilterType) { | ||
| let filterDate: object | undefined; | ||
| const currentDay = new Date(new Date().setHours(0, 0, 0, 0)); | ||
|
|
@@ -252,38 +279,43 @@ export class WinningsRepository { | |
| const totalItems = includeCount ? count : winnings.length; | ||
|
|
||
| result.data = { | ||
| winnings: winnings.map((item) => ({ | ||
| id: item.winning_id, | ||
| type: item.type, | ||
| winnerId: item.winner_id, | ||
| origin: item.origin?.origin_name, | ||
| category: (item.category ?? '') as WinningsCategory, | ||
| title: item.title as string, | ||
| description: item.description as string, | ||
| externalId: item.external_id as string, | ||
| attributes: (item.attributes ?? {}) as object, | ||
| details: item.payment?.map((paymentItem) => ({ | ||
| id: paymentItem.payment_id, | ||
| netAmount: Number(paymentItem.net_amount), | ||
| grossAmount: Number(paymentItem.gross_amount), | ||
| totalAmount: Number(paymentItem.total_amount), | ||
| installmentNumber: paymentItem.installment_number as number, | ||
| datePaid: (paymentItem.date_paid ?? undefined) as Date, | ||
| status: paymentItem.payment_status as PaymentStatus, | ||
| currency: paymentItem.currency as string, | ||
| releaseDate: paymentItem.release_date as Date, | ||
| category: item.category as string, | ||
| billingAccount: paymentItem.billing_account, | ||
| })), | ||
| createdAt: item.created_at as Date, | ||
| updatedAt: (item.payment?.[0].date_paid ?? | ||
| item.payment?.[0].updated_at ?? | ||
| undefined) as Date, | ||
| releaseDate: item.payment?.[0]?.release_date as Date, | ||
| paymentStatus: usersPayoutStatusMap[ | ||
| item.winner_id | ||
| ] as WinningDto['paymentStatus'], | ||
| })), | ||
| winnings: winnings.map((item) => { | ||
| const attributes = (item.attributes ?? {}) as object; | ||
|
|
||
| return { | ||
| id: item.winning_id, | ||
| type: item.type, | ||
| winnerId: item.winner_id, | ||
| origin: item.origin?.origin_name, | ||
| category: (item.category ?? '') as WinningsCategory, | ||
| title: item.title as string, | ||
| description: item.description as string, | ||
| externalId: item.external_id as string, | ||
| attributes, | ||
| hoursWorked: this.getHoursWorked(item.attributes), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| details: item.payment?.map((paymentItem) => ({ | ||
| id: paymentItem.payment_id, | ||
| netAmount: Number(paymentItem.net_amount), | ||
| grossAmount: Number(paymentItem.gross_amount), | ||
| totalAmount: Number(paymentItem.total_amount), | ||
| installmentNumber: paymentItem.installment_number as number, | ||
| datePaid: (paymentItem.date_paid ?? undefined) as Date, | ||
| status: paymentItem.payment_status as PaymentStatus, | ||
| currency: paymentItem.currency as string, | ||
| releaseDate: paymentItem.release_date as Date, | ||
| category: item.category as string, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| billingAccount: paymentItem.billing_account, | ||
| })), | ||
| createdAt: item.created_at as Date, | ||
| updatedAt: (item.payment?.[0].date_paid ?? | ||
| item.payment?.[0].updated_at ?? | ||
| undefined) as Date, | ||
| releaseDate: item.payment?.[0]?.release_date as Date, | ||
| paymentStatus: usersPayoutStatusMap[ | ||
| item.winner_id | ||
| ] as WinningDto['paymentStatus'], | ||
| }; | ||
| }), | ||
| pagination: { | ||
| totalItems, | ||
| totalPages: Math.ceil(totalItems / limit), | ||
|
|
@@ -344,36 +376,41 @@ export class WinningsRepository { | |
| ? await this.getUsersPayoutStatusForWinnings(winnings) | ||
| : ({} as { [key: string]: payment_status }); | ||
|
|
||
| result.data = winnings.map((item) => ({ | ||
| id: item.winning_id, | ||
| type: item.type, | ||
| winnerId: item.winner_id, | ||
| origin: item.origin?.origin_name, | ||
| category: (item.category ?? '') as WinningsCategory, | ||
| title: item.title as string, | ||
| description: item.description as string, | ||
| externalId: item.external_id as string, | ||
| attributes: (item.attributes ?? {}) as object, | ||
| details: item.payment?.map((paymentItem) => ({ | ||
| id: paymentItem.payment_id, | ||
| netAmount: Number(paymentItem.net_amount), | ||
| grossAmount: Number(paymentItem.gross_amount), | ||
| totalAmount: Number(paymentItem.total_amount), | ||
| installmentNumber: paymentItem.installment_number as number, | ||
| datePaid: (paymentItem.date_paid ?? undefined) as Date, | ||
| status: paymentItem.payment_status as PaymentStatus, | ||
| currency: paymentItem.currency as string, | ||
| releaseDate: paymentItem.release_date as Date, | ||
| category: item.category as string, | ||
| billingAccount: paymentItem.billing_account, | ||
| })), | ||
| createdAt: item.created_at as Date, | ||
| updatedAt: (item.payment?.[0].date_paid ?? | ||
| item.payment?.[0].updated_at ?? | ||
| undefined) as Date, | ||
| releaseDate: item.payment?.[0]?.release_date as Date, | ||
| paymentStatus: usersPayoutStatusMap[item.winner_id], | ||
| })); | ||
| result.data = winnings.map((item) => { | ||
| const attributes = (item.attributes ?? {}) as object; | ||
|
|
||
| return { | ||
| id: item.winning_id, | ||
| type: item.type, | ||
| winnerId: item.winner_id, | ||
| origin: item.origin?.origin_name, | ||
| category: (item.category ?? '') as WinningsCategory, | ||
| title: item.title as string, | ||
| description: item.description as string, | ||
| externalId: item.external_id as string, | ||
| attributes, | ||
| hoursWorked: this.getHoursWorked(item.attributes), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| details: item.payment?.map((paymentItem) => ({ | ||
| id: paymentItem.payment_id, | ||
| netAmount: Number(paymentItem.net_amount), | ||
| grossAmount: Number(paymentItem.gross_amount), | ||
| totalAmount: Number(paymentItem.total_amount), | ||
| installmentNumber: paymentItem.installment_number as number, | ||
| datePaid: (paymentItem.date_paid ?? undefined) as Date, | ||
| status: paymentItem.payment_status as PaymentStatus, | ||
| currency: paymentItem.currency as string, | ||
| releaseDate: paymentItem.release_date as Date, | ||
| category: item.category as string, | ||
| billingAccount: paymentItem.billing_account, | ||
| })), | ||
| createdAt: item.created_at as Date, | ||
| updatedAt: (item.payment?.[0].date_paid ?? | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| item.payment?.[0].updated_at ?? | ||
| undefined) as Date, | ||
| releaseDate: item.payment?.[0]?.release_date as Date, | ||
| paymentStatus: usersPayoutStatusMap[item.winner_id], | ||
| }; | ||
| }); | ||
| } catch (error) { | ||
| this.logger.error('Getting winnings by external ID failed', error); | ||
| const message = 'Getting winnings by external ID failed. ' + error; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,8 +76,9 @@ export class TaxFormHandler { | |
| }); | ||
|
|
||
| if (existingFormAssociation) { | ||
| const dateFiled = existingFormAssociation.date_filed.toISOString(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| this.logger.log( | ||
| `Found existing association id='${existingFormAssociation.id}' status='${existingFormAssociation.tax_form_status}' date_filed='${existingFormAssociation.date_filed}'`, | ||
| `Found existing association id='${existingFormAssociation.id}' status='${existingFormAssociation.tax_form_status}' date_filed='${dateFiled}'`, | ||
| ); | ||
| } else { | ||
| this.logger.log('No existing tax form association found'); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,6 +49,28 @@ export class WinningsService { | |
| private readonly tcEmailService: TopcoderEmailService, | ||
| ) {} | ||
|
|
||
| /** | ||
| * Builds the persisted winning attributes object. | ||
| * @param attributes Arbitrary attributes supplied by the client. | ||
| * @param hoursWorked Optional engagement-payment hours to persist. | ||
| * @returns The normalized attributes object, or `null` when empty. | ||
| */ | ||
| private buildWinningAttributes( | ||
| attributes: object | undefined, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| hoursWorked?: number, | ||
| ): Record<string, unknown> | null { | ||
| const normalizedAttributes = | ||
| attributes && typeof attributes === 'object' ? { ...attributes } : {}; | ||
|
|
||
| if (hoursWorked !== undefined) { | ||
| normalizedAttributes.hoursWorked = hoursWorked; | ||
| } | ||
|
|
||
| return Object.keys(normalizedAttributes).length | ||
| ? normalizedAttributes | ||
| : null; | ||
| } | ||
|
|
||
| private async sendSetupEmailNotification(userId: string, amount: number) { | ||
| this.logger.debug(`Fetching member info for user handle: ${userId}`); | ||
| const member = await this.tcMembersService.getMemberInfoByUserId(userId, { | ||
|
|
@@ -175,17 +197,17 @@ export class WinningsService { | |
| // if so, and the others are not matching the expectation, throw error | ||
| if ( | ||
| (body.type === WinningsType.POINTS || | ||
| body.category === winnings_category.POINTS_AWARD || | ||
| body.details.some((p) => p.currency === PrizeType.POINT)) && | ||
| body.category === WinningsCategory.POINTS_AWARD || | ||
| body.details.some((p) => String(p.currency) === 'POINT')) && | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| (body.type !== WinningsType.POINTS || | ||
| body.category !== winnings_category.POINTS_AWARD || | ||
| !body.details.some((p) => p.currency === PrizeType.POINT)) | ||
| body.category !== WinningsCategory.POINTS_AWARD || | ||
| !body.details.some((p) => String(p.currency) === 'POINT')) | ||
| ) { | ||
| const isTypePoints = body.type === WinningsType.POINTS; | ||
| const isCategoryPoints = | ||
| body.category === winnings_category.POINTS_AWARD; | ||
| body.category === WinningsCategory.POINTS_AWARD; | ||
| const hasPointsCurrency = body.details.some( | ||
| (p) => p.currency === PrizeType.POINT, | ||
| (p) => String(p.currency) === 'POINT', | ||
| ); | ||
|
|
||
| const mismatches: string[] = []; | ||
|
|
@@ -234,7 +256,10 @@ export class WinningsService { | |
| title: body.title, | ||
| description: body.description, | ||
| external_id: body.externalId, | ||
| attributes: body.attributes, | ||
| attributes: this.buildWinningAttributes( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| body.attributes, | ||
| body.hoursWorked, | ||
| ), | ||
| created_by: userId, | ||
| payment: { | ||
| create: [] as Pick< | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,6 @@ import { TopcoderMembersService } from 'src/shared/topcoder/members.service'; | |
| import { BasicMemberInfo, BASIC_MEMBER_FIELDS } from 'src/shared/topcoder'; | ||
| import { Logger } from 'src/shared/global'; | ||
| import { OtpService } from 'src/shared/global/otp.service'; | ||
| import { PrizeType } from '../challenges/models'; | ||
|
|
||
| const TROLLEY_MINIMUM_PAYMENT_AMOUNT = | ||
| ENV_CONFIG.TROLLEY_MINIMUM_PAYMENT_AMOUNT; | ||
|
|
@@ -78,7 +77,9 @@ export class WithdrawalService { | |
| // only USD payments can be withdrawn | ||
| if ( | ||
| winnings.some( | ||
| (w) => w.type !== winnings_type.PAYMENT || w.currency !== PrizeType.USD, | ||
| (w) => | ||
| String(w.type) !== winnings_type.PAYMENT || | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| String(w.currency) !== 'USD', | ||
| ) | ||
| ) { | ||
| throw new BadRequestException('Withdrawal supports USD payments only.'); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,8 @@ import { | |
| IsEnum, | ||
| IsObject, | ||
| ValidateNested, | ||
| IsNumber, | ||
| Min, | ||
| } from 'class-validator'; | ||
| import { PaginationInfo, SortPagination } from './sort-pagination.dto'; | ||
| import { DateFilterType } from './date-filter.type'; | ||
|
|
@@ -115,6 +117,7 @@ export class WinningDto { | |
| description: string; | ||
| externalId: string; | ||
| attributes: object; | ||
| hoursWorked?: number; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| details: PaymentDetailDto[]; | ||
| paymentStatus: PayoutStatus; | ||
| createdAt: Date; | ||
|
|
@@ -268,6 +271,17 @@ export class WinningCreateRequestDto { | |
| @IsObject() | ||
| attributes: object; | ||
|
|
||
| @ApiProperty({ | ||
| description: | ||
| 'Optional engagement-payment hours worked value. When provided, it is stored with the winning attributes and returned in payment history.', | ||
| example: 37.5, | ||
| required: false, | ||
| }) | ||
| @IsOptional() | ||
| @IsNumber() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| @Min(0.01) | ||
| hoursWorked?: number; | ||
|
|
||
| @ApiProperty({ | ||
| description: 'Optional payment status to apply to created payments', | ||
| enum: PaymentStatus, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[⚠️
maintainability]Consider using the
PaymentStatus.CANCELLEDenum instead of the string'CANCELLED'for consistency and to prevent potential typos or mismatches in the future.