Skip to content

Commit eca223e

Browse files
committed
Merge branch 'v6' of github.com:topcoder-platform/tc-finance-api into PM-2087_use-v6-apis
2 parents 41d75cc + 686991d commit eca223e

File tree

13 files changed

+204
-98
lines changed

13 files changed

+204
-98
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
Warnings:
3+
4+
- You are about to drop the `reward` table. If the table is not empty, all the data it contains will be lost.
5+
6+
*/
7+
-- DropForeignKey
8+
ALTER TABLE "reward" DROP CONSTRAINT "reward_winnings_id_fkey";
9+
10+
-- DropTable
11+
DROP TABLE "reward";
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
Warnings:
3+
4+
- You are about to drop the `payoneer_payment_method` table. If the table is not empty, all the data it contains will be lost.
5+
- You are about to drop the `paypal_payment_method` table. If the table is not empty, all the data it contains will be lost.
6+
7+
*/
8+
-- DropForeignKey
9+
ALTER TABLE "payoneer_payment_method" DROP CONSTRAINT "fk_payoneer_user_payment_method";
10+
11+
-- DropForeignKey
12+
ALTER TABLE "paypal_payment_method" DROP CONSTRAINT "fk_paypal_user_payment_method";
13+
14+
-- DropTable
15+
DROP TABLE "payoneer_payment_method";
16+
17+
-- DropTable
18+
DROP TABLE "paypal_payment_method";
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CREATE INDEX idx_payment_installment_number ON payment USING btree (installment_number);
2+
CREATE INDEX idx_payment_installment_status_version ON payment USING btree (installment_number, payment_status, winnings_id, version DESC);
3+
CREATE INDEX idx_payment_status_winnings ON payment USING btree (payment_status, winnings_id);
4+
CREATE INDEX idx_payment_win_inst_status ON payment USING btree (winnings_id, installment_number, payment_status);
5+
CREATE INDEX idx_payment_winnings_id ON payment USING btree (winnings_id);
6+
CREATE INDEX idx_payment_winnings_installment ON payment USING btree (winnings_id, installment_number);
7+
CREATE INDEX idx_payment_winnings_installment1_status ON payment USING btree (winnings_id, payment_status) WHERE (installment_number = 1);
8+
CREATE INDEX idx_winnings_category_created_at ON winnings USING btree (category, created_at DESC);
9+
CREATE INDEX idx_winnings_created_at ON winnings USING btree (created_at DESC);
10+
CREATE INDEX idx_winnings_external_id ON winnings USING btree (external_id) WHERE (external_id IS NOT NULL);
11+
CREATE INDEX idx_winnings_winner_created_at ON winnings USING btree (winner_id, created_at DESC);
12+
CREATE INDEX idx_winnings_winner_id_only ON winnings USING btree (winner_id) INCLUDE (winning_id);

prisma/schema.prisma

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ model payment {
5959
payment_method payment_method? @relation(fields: [payment_method_id], references: [payment_method_id], onDelete: NoAction, onUpdate: NoAction)
6060
winnings winnings @relation(fields: [winnings_id], references: [winning_id], onDelete: NoAction, onUpdate: NoAction)
6161
payment_release_associations payment_release_associations[]
62+
63+
@@index([installment_number], map: "idx_payment_installment_number")
64+
@@index([installment_number, payment_status, winnings_id, version(sort: Desc)], map: "idx_payment_installment_status_version")
65+
@@index([payment_status, winnings_id], map: "idx_payment_status_winnings")
66+
@@index([winnings_id, installment_number, payment_status], map: "idx_payment_win_inst_status")
67+
@@index([winnings_id], map: "idx_payment_winnings_id")
68+
@@index([winnings_id, installment_number], map: "idx_payment_winnings_installment")
6269
}
6370

6471
model payment_method {
@@ -96,45 +103,11 @@ model payment_releases {
96103
payment_method payment_method @relation(fields: [payment_method_id], references: [payment_method_id], onDelete: NoAction, onUpdate: NoAction)
97104
}
98105

99-
model payoneer_payment_method {
100-
id Int @id @default(autoincrement())
101-
user_payment_method_id String? @db.Uuid
102-
user_id String @unique @db.VarChar(80)
103-
payee_id String @db.VarChar(50)
104-
payoneer_id String? @db.VarChar(50)
105-
user_payment_methods user_payment_methods? @relation(fields: [user_payment_method_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "fk_payoneer_user_payment_method")
106-
}
107-
108-
model paypal_payment_method {
109-
id Int @id @default(autoincrement())
110-
user_payment_method_id String? @db.Uuid
111-
user_id String @unique @db.VarChar(80)
112-
email String? @db.VarChar(150)
113-
payer_id String? @db.VarChar(50)
114-
country_code String? @db.VarChar(2)
115-
user_payment_methods user_payment_methods? @relation(fields: [user_payment_method_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "fk_paypal_user_payment_method")
116-
}
117-
118-
model reward {
119-
reward_id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
120-
winnings_id String @db.Uuid
121-
points Int?
122-
title String? @db.VarChar(255)
123-
description String?
124-
reference Json?
125-
attributes Json?
126-
created_at DateTime? @default(now()) @db.Timestamp(6)
127-
updated_at DateTime? @default(now()) @db.Timestamp(6)
128-
winnings winnings @relation(fields: [winnings_id], references: [winning_id], onDelete: NoAction, onUpdate: NoAction)
129-
}
130-
131106
model user_payment_methods {
132107
id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
133108
user_id String @db.VarChar(80)
134109
payment_method_id Int
135110
status payment_method_status? @default(OTP_PENDING)
136-
payoneer_payment_method payoneer_payment_method[]
137-
paypal_payment_method paypal_payment_method[]
138111
trolley_payment_method trolley_recipient[]
139112
payment_method payment_method @relation(fields: [payment_method_id], references: [payment_method_id], onDelete: NoAction, onUpdate: NoAction, map: "fk_user_payment_method")
140113
@@ -165,8 +138,12 @@ model winnings {
165138
updated_at DateTime? @default(now()) @db.Timestamp(6)
166139
audit audit[]
167140
payment payment[]
168-
reward reward[]
169141
origin origin? @relation(fields: [origin_id], references: [origin_id], onDelete: NoAction, onUpdate: NoAction)
142+
143+
@@index([category, created_at(sort: Desc)], map: "idx_winnings_category_created_at")
144+
@@index([created_at(sort: Desc)], map: "idx_winnings_created_at")
145+
@@index([winner_id, created_at(sort: Desc)], map: "idx_winnings_winner_created_at")
146+
@@index([winner_id, winning_id], map: "idx_winnings_winner_id_only")
170147
}
171148

172149
model trolley_recipient {

src/api/challenges/challenges.service.ts

Lines changed: 77 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
import { includes, isEmpty, sortBy, find, camelCase, groupBy } from 'lodash';
1+
import { includes, isEmpty, find, camelCase, groupBy, orderBy } from 'lodash';
22
import { Injectable } from '@nestjs/common';
33
import { ENV_CONFIG } from 'src/config';
44
import { Logger } from 'src/shared/global';
5-
import { Challenge, ChallengeResource, ResourceRole } from './models';
5+
import {
6+
Challenge,
7+
ChallengeResource,
8+
ChallengeSubmission,
9+
ResourceRole,
10+
} from './models';
611
import { BillingAccountsService } from 'src/shared/topcoder/billing-accounts.service';
712
import { TopcoderM2MService } from 'src/shared/topcoder/topcoder-m2m.service';
813
import { ChallengeStatuses } from 'src/dto/challenge.dto';
@@ -40,7 +45,6 @@ export class ChallengesService {
4045

4146
try {
4247
const challenge = await this.m2MService.m2mFetch<Challenge>(requestUrl);
43-
this.logger.log(JSON.stringify(challenge, null, 2));
4448
return challenge;
4549
} catch (e) {
4650
this.logger.error(
@@ -50,6 +54,24 @@ export class ChallengesService {
5054
}
5155
}
5256

57+
async getChallengeSubmissionsCount(challengeId: string) {
58+
const requestUrl = `${TC_API_BASE}/submissions?challengeId=${challengeId}&perPage=9999`;
59+
60+
try {
61+
const submissions =
62+
await this.m2MService.m2mFetch<ChallengeSubmission[]>(requestUrl);
63+
const uniqueSubmissions = Object.fromEntries(
64+
submissions.map((s) => [s.memberId, s]),
65+
);
66+
return Object.keys(uniqueSubmissions).length;
67+
} catch (e) {
68+
this.logger.error(
69+
`Challenge submissions couldn't be fetched for challenge ${challengeId}!`,
70+
e,
71+
);
72+
}
73+
}
74+
5375
async getChallengeResources(challengeId: string) {
5476
try {
5577
const resources = await this.m2MService.m2mFetch<ChallengeResource[]>(
@@ -97,36 +119,43 @@ export class ChallengesService {
97119
}[];
98120

99121
const { prizeSets, winners, reviewers } = challenge;
122+
const isCancelledFailedReview =
123+
challenge.status.toLowerCase() ===
124+
ChallengeStatuses.CancelledFailedReview.toLowerCase();
100125

101126
// generate placement payments
102-
const placementPrizes = sortBy(
127+
const placementPrizes = orderBy(
103128
find(prizeSets, { type: 'PLACEMENT' })?.prizes,
104129
'value',
130+
'desc',
105131
);
106-
if (placementPrizes.length < winners.length) {
107-
throw new Error(
108-
'Task has incorrect number of placement prizes! There are more winners than prizes!',
109-
);
110-
}
111132

112-
winners.forEach((winner) => {
113-
payments.push({
114-
handle: winner.handle,
115-
amount: placementPrizes[winner.placement - 1].value,
116-
userId: winner.userId.toString(),
117-
type: challenge.task.isTask
118-
? WinningsCategory.TASK_PAYMENT
119-
: WinningsCategory.CONTEST_PAYMENT,
120-
description:
121-
challenge.type === 'Task'
122-
? challenge.name
123-
: `${challenge.name} - ${placeToOrdinal(winner.placement)} Place`,
133+
if (!isCancelledFailedReview) {
134+
if (placementPrizes.length < winners.length) {
135+
throw new Error(
136+
'Task has incorrect number of placement prizes! There are more winners than prizes!',
137+
);
138+
}
139+
140+
winners.forEach((winner) => {
141+
payments.push({
142+
handle: winner.handle,
143+
amount: placementPrizes[winner.placement - 1].value,
144+
userId: winner.userId.toString(),
145+
type: challenge.task.isTask
146+
? WinningsCategory.TASK_PAYMENT
147+
: WinningsCategory.CONTEST_PAYMENT,
148+
description:
149+
challenge.type === 'Task'
150+
? challenge.name
151+
: `${challenge.name} - ${placeToOrdinal(winner.placement)} Place`,
152+
});
124153
});
125-
});
154+
}
126155

127156
// generate copilot payments
128157
const copilotPrizes = find(prizeSets, { type: 'COPILOT' })?.prizes ?? [];
129-
if (copilotPrizes.length) {
158+
if (copilotPrizes.length && !isCancelledFailedReview) {
130159
const copilots = challengeResources.copilot;
131160

132161
if (!copilots?.length) {
@@ -144,19 +173,22 @@ export class ChallengesService {
144173
}
145174

146175
// generate reviewer payments
147-
const firstPlacePrize = placementPrizes[0].value;
176+
const firstPlacePrize = placementPrizes?.[0]?.value ?? 0;
148177
const challengeReviewer = find(reviewers, { isMemberReview: true });
178+
const numOfSubmissions =
179+
(await this.getChallengeSubmissionsCount(challenge.id)) ?? 1;
149180

150181
if (challengeReviewer && challengeResources.reviewer) {
151182
challengeResources.reviewer?.forEach((reviewer) => {
152183
payments.push({
153184
handle: reviewer.memberHandle,
154185
userId: reviewer.memberId.toString(),
155186
amount: Math.round(
156-
(challengeReviewer.basePayment ?? 0) +
157-
(challengeReviewer.incrementalPayment ?? 0) *
158-
challenge.numOfSubmissions *
159-
firstPlacePrize,
187+
(challengeReviewer.fixedAmount ?? 0) +
188+
(challengeReviewer.baseCoefficient ?? 0) * firstPlacePrize +
189+
(challengeReviewer.incrementalCoefficient ?? 0) *
190+
firstPlacePrize *
191+
numOfSubmissions,
160192
),
161193
type: WinningsCategory.REVIEW_BOARD_PAYMENT,
162194
});
@@ -202,18 +234,18 @@ export class ChallengesService {
202234
throw new Error('Challenge not found!');
203235
}
204236

205-
if (
206-
challenge.status.toLowerCase() !==
207-
ChallengeStatuses.Completed.toLowerCase()
208-
) {
209-
throw new Error("Challenge isn't completed yet!");
237+
const allowedStatuses = [
238+
ChallengeStatuses.Completed.toLowerCase(),
239+
ChallengeStatuses.CancelledFailedReview.toLowerCase(),
240+
];
241+
242+
if (!allowedStatuses.includes(challenge.status.toLowerCase())) {
243+
throw new Error("Challenge isn't in a payable status!");
210244
}
211245

212246
const existingPayments = (
213247
await this.winningsRepo.searchWinnings({
214248
externalIds: [challengeId],
215-
limit: 1,
216-
offset: 0,
217249
} as WinningRequestDto)
218250
)?.data?.winnings;
219251
if (existingPayments?.length > 0) {
@@ -244,9 +276,16 @@ export class ChallengesService {
244276
}
245277

246278
await Promise.all(
247-
payments.map((p) =>
248-
this.winningsService.createWinningWithPayments(p, userId),
249-
),
279+
payments.map(async (p) => {
280+
try {
281+
await this.winningsService.createWinningWithPayments(p, userId);
282+
} catch (e) {
283+
this.logger.log(
284+
`Failed to create winnings payment for user ${p.winnerId}!`,
285+
e,
286+
);
287+
}
288+
}),
250289
);
251290

252291
this.logger.log('Task Completed. locking consumed budget', baValidation);

src/api/challenges/models/challenge.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,9 @@ export interface Reviewer {
126126
scorecardId: string;
127127
isMemberReview: boolean;
128128
memberReviewerCount?: number;
129-
basePayment?: number;
130-
incrementalPayment?: number;
129+
baseCoefficient?: number;
130+
incrementalCoefficient?: number;
131+
fixedAmount?: number;
131132
isAIReviewer: boolean;
132133
}
133134

@@ -141,3 +142,9 @@ export interface ResourceRole {
141142
id: string;
142143
name: string;
143144
}
145+
146+
export interface ChallengeSubmission {
147+
id: string;
148+
memberId: string;
149+
type: 'CONTEST_SUBMISSION';
150+
}

src/api/wallet/wallet.service.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ export class WalletService {
133133
w.type AS payment_type,
134134
CASE
135135
WHEN w.type = 'PAYMENT' THEN SUM(p.total_amount)
136-
WHEN w.type = 'REWARD' THEN SUM(r.points)
137136
ELSE 0
138137
END AS total_owed
139138
FROM
@@ -144,7 +143,6 @@ export class WalletService {
144143
AND p.installment_number = 1
145144
INNER JOIN latest_payment_version lpv ON p.winnings_id = lpv.winnings_id
146145
AND p.version = lpv.max_version
147-
LEFT JOIN reward r ON w.winning_id = r.winnings_id
148146
AND w.type = 'REWARD'
149147
WHERE
150148
w.winner_id = ${winnerId}

src/api/winnings/winnings.controller.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import {
77
ApiBearerAuth,
88
} from '@nestjs/swagger';
99

10-
import { M2mScope } from 'src/core/auth/auth.constants';
11-
import { M2M, AllowedM2mScope, User } from 'src/core/auth/decorators';
10+
import { M2mScope, Role } from 'src/core/auth/auth.constants';
11+
import { AllowedM2mScope, User, Roles, M2M } from 'src/core/auth/decorators';
1212
import { ResponseDto, ResponseStatusType } from 'src/dto/api-response.dto';
1313
import { UserInfo } from 'src/dto/user.type';
1414
import {
@@ -29,8 +29,8 @@ export class WinningsController {
2929
) {}
3030

3131
@Post()
32-
@M2M()
3332
@AllowedM2mScope(M2mScope.CreatePayments)
33+
@Roles(Role.PaymentAdmin, Role.PaymentEditor)
3434
@ApiOperation({
3535
summary: 'Create winning with payments.',
3636
description: 'User must have "create:payments" scope to access.',

src/api/winnings/winnings.service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class WinningsService {
5656
}
5757

5858
this.logger.debug(
59-
`Member info retrieved successfully for user handle: ${userId}`,
59+
`Member info retrieved successfully for user: ${userId}`,
6060
{ member },
6161
);
6262

@@ -142,7 +142,7 @@ export class WinningsService {
142142
const result = new ResponseDto<string>();
143143

144144
this.logger.debug(
145-
`Creating winning with payments for user ${userId}`,
145+
`Creating winning with payments for user ${body.winnerId}`,
146146
body,
147147
);
148148

@@ -186,7 +186,7 @@ export class WinningsService {
186186
);
187187
const isIdentityVerified =
188188
await this.identityVerificationRepo.completedIdentityVerification(
189-
userId,
189+
body.winnerId,
190190
);
191191

192192
for (const detail of body.details || []) {

0 commit comments

Comments
 (0)