Skip to content

Commit 0ed0b02

Browse files
authored
Merge pull request #148 from topcoder-platform/dev
Engagements v4
2 parents 3e1bf7a + 6102f01 commit 0ed0b02

8 files changed

Lines changed: 152 additions & 78 deletions

File tree

src/api/admin/admin.service.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,7 @@ export class AdminService {
168168
`Processing payment ${payment.payment_id} (installment ${payment.installment_number}) with current status=${payment.payment_status}`,
169169
);
170170

171-
if (
172-
payment.payment_status &&
173-
payment.payment_status === PaymentStatus.CANCELLED
174-
) {
171+
if (payment.payment_status && payment.payment_status === 'CANCELLED') {
175172
this.logger.warn(
176173
`Attempt to update cancelled payment ${payment.payment_id} — rejecting`,
177174
);

src/api/repository/winnings.repo.ts

Lines changed: 99 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,33 @@ export class WinningsRepository {
3333

3434
constructor(private readonly prisma: PrismaService) {}
3535

36+
/**
37+
* Extracts the optional hours-worked value from winning attributes.
38+
* @param attributes Winning attributes payload retrieved from the database.
39+
* @returns Parsed hours-worked value, or `undefined` when absent/invalid.
40+
*/
41+
private getHoursWorked(
42+
attributes: Prisma.JsonValue | null,
43+
): number | undefined {
44+
if (
45+
!attributes ||
46+
typeof attributes !== 'object' ||
47+
Array.isArray(attributes)
48+
) {
49+
return undefined;
50+
}
51+
52+
const value = (attributes as Record<string, unknown>).hoursWorked;
53+
if (value === undefined || value === null || value === '') {
54+
return undefined;
55+
}
56+
57+
const parsedValue = Number(value);
58+
return Number.isFinite(parsedValue) && parsedValue > 0
59+
? parsedValue
60+
: undefined;
61+
}
62+
3663
private generateFilterDate(date?: DateFilterType) {
3764
let filterDate: object | undefined;
3865
const currentDay = new Date(new Date().setHours(0, 0, 0, 0));
@@ -252,38 +279,43 @@ export class WinningsRepository {
252279
const totalItems = includeCount ? count : winnings.length;
253280

254281
result.data = {
255-
winnings: winnings.map((item) => ({
256-
id: item.winning_id,
257-
type: item.type,
258-
winnerId: item.winner_id,
259-
origin: item.origin?.origin_name,
260-
category: (item.category ?? '') as WinningsCategory,
261-
title: item.title as string,
262-
description: item.description as string,
263-
externalId: item.external_id as string,
264-
attributes: (item.attributes ?? {}) as object,
265-
details: item.payment?.map((paymentItem) => ({
266-
id: paymentItem.payment_id,
267-
netAmount: Number(paymentItem.net_amount),
268-
grossAmount: Number(paymentItem.gross_amount),
269-
totalAmount: Number(paymentItem.total_amount),
270-
installmentNumber: paymentItem.installment_number as number,
271-
datePaid: (paymentItem.date_paid ?? undefined) as Date,
272-
status: paymentItem.payment_status as PaymentStatus,
273-
currency: paymentItem.currency as string,
274-
releaseDate: paymentItem.release_date as Date,
275-
category: item.category as string,
276-
billingAccount: paymentItem.billing_account,
277-
})),
278-
createdAt: item.created_at as Date,
279-
updatedAt: (item.payment?.[0].date_paid ??
280-
item.payment?.[0].updated_at ??
281-
undefined) as Date,
282-
releaseDate: item.payment?.[0]?.release_date as Date,
283-
paymentStatus: usersPayoutStatusMap[
284-
item.winner_id
285-
] as WinningDto['paymentStatus'],
286-
})),
282+
winnings: winnings.map((item) => {
283+
const attributes = (item.attributes ?? {}) as object;
284+
285+
return {
286+
id: item.winning_id,
287+
type: item.type,
288+
winnerId: item.winner_id,
289+
origin: item.origin?.origin_name,
290+
category: (item.category ?? '') as WinningsCategory,
291+
title: item.title as string,
292+
description: item.description as string,
293+
externalId: item.external_id as string,
294+
attributes,
295+
hoursWorked: this.getHoursWorked(item.attributes),
296+
details: item.payment?.map((paymentItem) => ({
297+
id: paymentItem.payment_id,
298+
netAmount: Number(paymentItem.net_amount),
299+
grossAmount: Number(paymentItem.gross_amount),
300+
totalAmount: Number(paymentItem.total_amount),
301+
installmentNumber: paymentItem.installment_number as number,
302+
datePaid: (paymentItem.date_paid ?? undefined) as Date,
303+
status: paymentItem.payment_status as PaymentStatus,
304+
currency: paymentItem.currency as string,
305+
releaseDate: paymentItem.release_date as Date,
306+
category: item.category as string,
307+
billingAccount: paymentItem.billing_account,
308+
})),
309+
createdAt: item.created_at as Date,
310+
updatedAt: (item.payment?.[0].date_paid ??
311+
item.payment?.[0].updated_at ??
312+
undefined) as Date,
313+
releaseDate: item.payment?.[0]?.release_date as Date,
314+
paymentStatus: usersPayoutStatusMap[
315+
item.winner_id
316+
] as WinningDto['paymentStatus'],
317+
};
318+
}),
287319
pagination: {
288320
totalItems,
289321
totalPages: Math.ceil(totalItems / limit),
@@ -344,36 +376,41 @@ export class WinningsRepository {
344376
? await this.getUsersPayoutStatusForWinnings(winnings)
345377
: ({} as { [key: string]: payment_status });
346378

347-
result.data = winnings.map((item) => ({
348-
id: item.winning_id,
349-
type: item.type,
350-
winnerId: item.winner_id,
351-
origin: item.origin?.origin_name,
352-
category: (item.category ?? '') as WinningsCategory,
353-
title: item.title as string,
354-
description: item.description as string,
355-
externalId: item.external_id as string,
356-
attributes: (item.attributes ?? {}) as object,
357-
details: item.payment?.map((paymentItem) => ({
358-
id: paymentItem.payment_id,
359-
netAmount: Number(paymentItem.net_amount),
360-
grossAmount: Number(paymentItem.gross_amount),
361-
totalAmount: Number(paymentItem.total_amount),
362-
installmentNumber: paymentItem.installment_number as number,
363-
datePaid: (paymentItem.date_paid ?? undefined) as Date,
364-
status: paymentItem.payment_status as PaymentStatus,
365-
currency: paymentItem.currency as string,
366-
releaseDate: paymentItem.release_date as Date,
367-
category: item.category as string,
368-
billingAccount: paymentItem.billing_account,
369-
})),
370-
createdAt: item.created_at as Date,
371-
updatedAt: (item.payment?.[0].date_paid ??
372-
item.payment?.[0].updated_at ??
373-
undefined) as Date,
374-
releaseDate: item.payment?.[0]?.release_date as Date,
375-
paymentStatus: usersPayoutStatusMap[item.winner_id],
376-
}));
379+
result.data = winnings.map((item) => {
380+
const attributes = (item.attributes ?? {}) as object;
381+
382+
return {
383+
id: item.winning_id,
384+
type: item.type,
385+
winnerId: item.winner_id,
386+
origin: item.origin?.origin_name,
387+
category: (item.category ?? '') as WinningsCategory,
388+
title: item.title as string,
389+
description: item.description as string,
390+
externalId: item.external_id as string,
391+
attributes,
392+
hoursWorked: this.getHoursWorked(item.attributes),
393+
details: item.payment?.map((paymentItem) => ({
394+
id: paymentItem.payment_id,
395+
netAmount: Number(paymentItem.net_amount),
396+
grossAmount: Number(paymentItem.gross_amount),
397+
totalAmount: Number(paymentItem.total_amount),
398+
installmentNumber: paymentItem.installment_number as number,
399+
datePaid: (paymentItem.date_paid ?? undefined) as Date,
400+
status: paymentItem.payment_status as PaymentStatus,
401+
currency: paymentItem.currency as string,
402+
releaseDate: paymentItem.release_date as Date,
403+
category: item.category as string,
404+
billingAccount: paymentItem.billing_account,
405+
})),
406+
createdAt: item.created_at as Date,
407+
updatedAt: (item.payment?.[0].date_paid ??
408+
item.payment?.[0].updated_at ??
409+
undefined) as Date,
410+
releaseDate: item.payment?.[0]?.release_date as Date,
411+
paymentStatus: usersPayoutStatusMap[item.winner_id],
412+
};
413+
});
377414
} catch (error) {
378415
this.logger.error('Getting winnings by external ID failed', error);
379416
const message = 'Getting winnings by external ID failed. ' + error;

src/api/webhooks/trolley/handlers/tax-form.handler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ export class TaxFormHandler {
7676
});
7777

7878
if (existingFormAssociation) {
79+
const dateFiled = existingFormAssociation.date_filed.toISOString();
7980
this.logger.log(
80-
`Found existing association id='${existingFormAssociation.id}' status='${existingFormAssociation.tax_form_status}' date_filed='${existingFormAssociation.date_filed}'`,
81+
`Found existing association id='${existingFormAssociation.id}' status='${existingFormAssociation.tax_form_status}' date_filed='${dateFiled}'`,
8182
);
8283
} else {
8384
this.logger.log('No existing tax form association found');

src/api/winnings/winnings.service.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,28 @@ export class WinningsService {
4949
private readonly tcEmailService: TopcoderEmailService,
5050
) {}
5151

52+
/**
53+
* Builds the persisted winning attributes object.
54+
* @param attributes Arbitrary attributes supplied by the client.
55+
* @param hoursWorked Optional engagement-payment hours to persist.
56+
* @returns The normalized attributes object, or `null` when empty.
57+
*/
58+
private buildWinningAttributes(
59+
attributes: object | undefined,
60+
hoursWorked?: number,
61+
): Record<string, unknown> | null {
62+
const normalizedAttributes =
63+
attributes && typeof attributes === 'object' ? { ...attributes } : {};
64+
65+
if (hoursWorked !== undefined) {
66+
normalizedAttributes.hoursWorked = hoursWorked;
67+
}
68+
69+
return Object.keys(normalizedAttributes).length
70+
? normalizedAttributes
71+
: null;
72+
}
73+
5274
private async sendSetupEmailNotification(userId: string, amount: number) {
5375
this.logger.debug(`Fetching member info for user handle: ${userId}`);
5476
const member = await this.tcMembersService.getMemberInfoByUserId(userId, {
@@ -175,17 +197,17 @@ export class WinningsService {
175197
// if so, and the others are not matching the expectation, throw error
176198
if (
177199
(body.type === WinningsType.POINTS ||
178-
body.category === winnings_category.POINTS_AWARD ||
179-
body.details.some((p) => p.currency === PrizeType.POINT)) &&
200+
body.category === WinningsCategory.POINTS_AWARD ||
201+
body.details.some((p) => String(p.currency) === 'POINT')) &&
180202
(body.type !== WinningsType.POINTS ||
181-
body.category !== winnings_category.POINTS_AWARD ||
182-
!body.details.some((p) => p.currency === PrizeType.POINT))
203+
body.category !== WinningsCategory.POINTS_AWARD ||
204+
!body.details.some((p) => String(p.currency) === 'POINT'))
183205
) {
184206
const isTypePoints = body.type === WinningsType.POINTS;
185207
const isCategoryPoints =
186-
body.category === winnings_category.POINTS_AWARD;
208+
body.category === WinningsCategory.POINTS_AWARD;
187209
const hasPointsCurrency = body.details.some(
188-
(p) => p.currency === PrizeType.POINT,
210+
(p) => String(p.currency) === 'POINT',
189211
);
190212

191213
const mismatches: string[] = [];
@@ -234,7 +256,10 @@ export class WinningsService {
234256
title: body.title,
235257
description: body.description,
236258
external_id: body.externalId,
237-
attributes: body.attributes,
259+
attributes: this.buildWinningAttributes(
260+
body.attributes,
261+
body.hoursWorked,
262+
),
238263
created_by: userId,
239264
payment: {
240265
create: [] as Pick<

src/api/withdrawal/withdrawal.service.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { TopcoderMembersService } from 'src/shared/topcoder/members.service';
1717
import { BasicMemberInfo, BASIC_MEMBER_FIELDS } from 'src/shared/topcoder';
1818
import { Logger } from 'src/shared/global';
1919
import { OtpService } from 'src/shared/global/otp.service';
20-
import { PrizeType } from '../challenges/models';
2120

2221
const TROLLEY_MINIMUM_PAYMENT_AMOUNT =
2322
ENV_CONFIG.TROLLEY_MINIMUM_PAYMENT_AMOUNT;
@@ -78,7 +77,9 @@ export class WithdrawalService {
7877
// only USD payments can be withdrawn
7978
if (
8079
winnings.some(
81-
(w) => w.type !== winnings_type.PAYMENT || w.currency !== PrizeType.USD,
80+
(w) =>
81+
String(w.type) !== winnings_type.PAYMENT ||
82+
String(w.currency) !== 'USD',
8283
)
8384
) {
8485
throw new BadRequestException('Withdrawal supports USD payments only.');

src/dto/payment.dto.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
IsInt,
66
IsString,
77
IsNotEmpty,
8-
isEnum,
98
IsEnum,
109
} from 'class-validator';
1110
import { PrizeType } from 'src/api/challenges/models';

src/dto/winning.dto.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
IsEnum,
99
IsObject,
1010
ValidateNested,
11+
IsNumber,
12+
Min,
1113
} from 'class-validator';
1214
import { PaginationInfo, SortPagination } from './sort-pagination.dto';
1315
import { DateFilterType } from './date-filter.type';
@@ -115,6 +117,7 @@ export class WinningDto {
115117
description: string;
116118
externalId: string;
117119
attributes: object;
120+
hoursWorked?: number;
118121
details: PaymentDetailDto[];
119122
paymentStatus: PayoutStatus;
120123
createdAt: Date;
@@ -268,6 +271,17 @@ export class WinningCreateRequestDto {
268271
@IsObject()
269272
attributes: object;
270273

274+
@ApiProperty({
275+
description:
276+
'Optional engagement-payment hours worked value. When provided, it is stored with the winning attributes and returned in payment history.',
277+
example: 37.5,
278+
required: false,
279+
})
280+
@IsOptional()
281+
@IsNumber()
282+
@Min(0.01)
283+
hoursWorked?: number;
284+
271285
@ApiProperty({
272286
description: 'Optional payment status to apply to created payments',
273287
enum: PaymentStatus,

src/shared/global/ba-prisma.client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const clientOptions = {
1515
};
1616

1717
let baPrismaClient: BaPrismaClient;
18-
export const getBaClient = () => {
18+
export const getBaClient = (): BaPrismaClient => {
1919
if (!baPrismaClient) {
2020
if (!ENV_CONFIG.BILLING_ACCOUNTS_DB_URL) {
2121
throw new Error(

0 commit comments

Comments
 (0)