Skip to content

Commit 993080e

Browse files
authored
Merge pull request #181 from topcoder-platform/dev
Prod release - BA visibility tweaks May 2026
2 parents 08d7e8a + 12666cc commit 993080e

5 files changed

Lines changed: 220 additions & 17 deletions

File tree

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ ENV PRISMA_CLI_BINARY_TARGETS=linux-musl-openssl-3.0.x
1313

1414
WORKDIR /app
1515
COPY . .
16-
RUN npm install pnpm -g
17-
RUN pnpm install
16+
RUN npm install pnpm@9.15.9 -g
17+
RUN pnpm install --frozen-lockfile --prod=false
1818
RUN pnpm run build
1919
RUN chmod +x appStartUp.sh
20-
CMD ./appStartUp.sh
20+
CMD ["./appStartUp.sh"]

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"description": "",
55
"author": "",
66
"private": true,
7+
"packageManager": "pnpm@9.15.9",
78
"license": "UNLICENSED",
89
"scripts": {
910
"build": "nest build",

src/api/admin/admin.service.spec.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,158 @@ describe('AdminService', () => {
515515
expect(baService.lockConsumeAmount).not.toHaveBeenCalled();
516516
});
517517

518+
it('recalculates the challenge billing-account line item when a challenge payment amount is adjusted', async () => {
519+
prisma.payment.findMany
520+
.mockResolvedValueOnce([
521+
{
522+
billing_account: '80001012',
523+
currency: 'USD',
524+
installment_number: 1,
525+
payment_id: 'payment-1',
526+
payment_status: PaymentStatus.OWED,
527+
release_date: new Date('2026-04-27T00:00:00.000Z'),
528+
total_amount: '100.00',
529+
version: 1,
530+
winnings: {
531+
category: 'CONTEST_PAYMENT',
532+
description: 'Challenge payment',
533+
external_id: 'challenge-1',
534+
type: 'PAYMENT',
535+
},
536+
},
537+
])
538+
.mockResolvedValueOnce([
539+
{ total_amount: '150.00' },
540+
{ total_amount: '25.00' },
541+
]);
542+
topcoderChallengesService.getChallengeById.mockResolvedValue({
543+
billing: {
544+
billingAccountId: '80001012',
545+
markup: 0.1,
546+
},
547+
id: 'challenge-1',
548+
status: 'COMPLETED',
549+
});
550+
551+
const result = await service.updateWinnings(
552+
{
553+
paymentAmount: 150,
554+
paymentId: 'payment-1',
555+
winningsId: 'winning-1',
556+
} as any,
557+
'admin-1',
558+
['Payment Admin'],
559+
);
560+
561+
expect(result.data).toBe('Successfully updated winnings');
562+
expect(prisma.payment.update).toHaveBeenCalledWith({
563+
where: {
564+
payment_id: 'payment-1',
565+
winnings_id: 'winning-1',
566+
version: 1,
567+
payment_status: {
568+
in: [
569+
PaymentStatus.CREDITED,
570+
PaymentStatus.OWED,
571+
PaymentStatus.ON_HOLD,
572+
PaymentStatus.ON_HOLD_ADMIN,
573+
PaymentStatus.PAID,
574+
PaymentStatus.PROCESSING,
575+
],
576+
},
577+
},
578+
data: {
579+
challenge_fee: undefined,
580+
gross_amount: 150,
581+
net_amount: 150,
582+
total_amount: 150,
583+
updated_at: expect.any(Date),
584+
updated_by: 'admin-1',
585+
version: 2,
586+
},
587+
});
588+
expect(baService.lockConsumeAmount).toHaveBeenCalledWith({
589+
billingAccountId: 80001012,
590+
challengeId: 'challenge-1',
591+
markup: 0.1,
592+
status: 'COMPLETED',
593+
totalPrizesInCents: 17500,
594+
});
595+
});
596+
597+
it('updates engagement challenge fee and consumed rows when an engagement payment amount is adjusted', async () => {
598+
prisma.payment.findMany
599+
.mockResolvedValueOnce([
600+
{
601+
billing_account: '80001012',
602+
challenge_markup: '0.20',
603+
currency: 'USD',
604+
installment_number: 1,
605+
payment_id: 'payment-1',
606+
payment_status: PaymentStatus.OWED,
607+
release_date: new Date('2026-04-28T00:00:00.000Z'),
608+
total_amount: '100.00',
609+
version: 1,
610+
winnings: {
611+
category: 'ENGAGEMENT_PAYMENT',
612+
description: 'Engagement payment',
613+
external_id: 'assignment-1',
614+
type: 'PAYMENT',
615+
},
616+
},
617+
])
618+
.mockResolvedValueOnce([
619+
{
620+
challenge_fee: '30.00',
621+
total_amount: '150.00',
622+
},
623+
]);
624+
625+
const result = await service.updateWinnings(
626+
{
627+
paymentAmount: 150,
628+
paymentId: 'payment-1',
629+
winningsId: 'winning-1',
630+
} as any,
631+
'admin-1',
632+
['Payment Admin'],
633+
);
634+
635+
expect(result.data).toBe('Successfully updated winnings');
636+
expect(prisma.payment.update).toHaveBeenCalledWith({
637+
where: {
638+
payment_id: 'payment-1',
639+
winnings_id: 'winning-1',
640+
version: 1,
641+
payment_status: {
642+
in: [
643+
PaymentStatus.CREDITED,
644+
PaymentStatus.OWED,
645+
PaymentStatus.ON_HOLD,
646+
PaymentStatus.ON_HOLD_ADMIN,
647+
PaymentStatus.PAID,
648+
PaymentStatus.PROCESSING,
649+
],
650+
},
651+
},
652+
data: {
653+
challenge_fee: 30,
654+
gross_amount: 150,
655+
net_amount: 150,
656+
total_amount: 150,
657+
updated_at: expect.any(Date),
658+
updated_by: 'admin-1',
659+
version: 2,
660+
},
661+
});
662+
expect(baService.syncEngagementConsumeAmounts).toHaveBeenCalledWith({
663+
amounts: [180],
664+
billingAccountId: 80001012,
665+
externalId: 'assignment-1',
666+
});
667+
expect(baService.lockConsumeAmount).not.toHaveBeenCalled();
668+
});
669+
518670
it('returns task details for task payment with projectId and approver', async () => {
519671
prisma.winnings.findFirst.mockResolvedValue({
520672
winning_id: 'winning-task',

src/api/admin/admin.service.ts

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ export class AdminService {
353353

354354
/**
355355
* Finds challenge billing-account rows that need to be recalculated after a
356-
* wallet-admin payment status change.
356+
* wallet-admin payment status or amount change.
357357
*
358358
* @param payments payment rows selected by the update request.
359359
* @returns unique challenge and billing-account pairs touched by USD
@@ -402,7 +402,7 @@ export class AdminService {
402402

403403
/**
404404
* Finds engagement billing-account rows that need to be reconciled after a
405-
* wallet-admin payment status change.
405+
* wallet-admin payment status or amount change.
406406
*
407407
* @param payments payment rows selected by the update request.
408408
* @returns unique engagement assignment and billing-account pairs touched by
@@ -539,6 +539,39 @@ export class AdminService {
539539
);
540540
}
541541

542+
/**
543+
* Recomputes the persisted engagement billing fee after an admin amount edit.
544+
*
545+
* Engagement consumed rows store payment total plus `payment.challenge_fee`.
546+
* When wallet admin adjusts the payment total, the fee must be recalculated
547+
* from the persisted markup before finance resyncs the BA consumed row.
548+
*
549+
* @param category winning category for the payment being edited.
550+
* @param challengeMarkup markup persisted on the payment row.
551+
* @param totalAmount new payment total amount.
552+
* @returns recalculated fee for engagement payments, or `undefined` when the
553+
* payment is not an engagement payment or has no valid persisted markup.
554+
* @throws This helper does not throw.
555+
*/
556+
private calculateAdjustedChallengeFee(
557+
category: winnings_category | null,
558+
challengeMarkup: Prisma.Decimal | number | string | null,
559+
totalAmount: number,
560+
): number | undefined {
561+
if (category !== winnings_category.ENGAGEMENT_PAYMENT) {
562+
return undefined;
563+
}
564+
565+
const markup = Number(challengeMarkup);
566+
if (!Number.isFinite(markup) || markup < 0) {
567+
return undefined;
568+
}
569+
570+
return this.toPaymentAmount(
571+
new Prisma.Decimal(totalAmount).mul(new Prisma.Decimal(markup)),
572+
);
573+
}
574+
542575
/**
543576
* Sums non-cancelled USD payment rows for a challenge and billing account.
544577
*
@@ -574,8 +607,8 @@ export class AdminService {
574607
}
575608

576609
/**
577-
* Rewrites challenge billing-account budget rows after a wallet-admin status
578-
* update changes the active payment total.
610+
* Rewrites challenge billing-account budget rows after a wallet-admin update
611+
* changes the active payment total.
579612
*
580613
* @param targets unique challenge and billing-account pairs to synchronize.
581614
* @returns promise resolved after every target has been sent to BA.
@@ -663,7 +696,7 @@ export class AdminService {
663696

664697
/**
665698
* Reconciles engagement billing-account consumed rows after a wallet-admin
666-
* cancellation changes the active payment set.
699+
* update changes the active payment amounts or active payment set.
667700
*
668701
* @param targets unique engagement assignment and billing-account pairs to
669702
* synchronize.
@@ -780,14 +813,15 @@ export class AdminService {
780813
tx: Prisma.TransactionClient,
781814
) => Promise<unknown>)[] = [];
782815
const now = new Date().getTime();
783-
const challengeBudgetSyncTargets =
784-
body.paymentStatus === PaymentStatus.CANCELLED
785-
? this.getChallengeBudgetSyncTargets(payments)
786-
: [];
787-
const engagementBudgetSyncTargets =
788-
body.paymentStatus === PaymentStatus.CANCELLED
789-
? this.getEngagementBudgetSyncTargets(payments)
790-
: [];
816+
const shouldSyncBudget =
817+
body.paymentStatus === PaymentStatus.CANCELLED ||
818+
body.paymentAmount !== undefined;
819+
const challengeBudgetSyncTargets = shouldSyncBudget
820+
? this.getChallengeBudgetSyncTargets(payments)
821+
: [];
822+
const engagementBudgetSyncTargets = shouldSyncBudget
823+
? this.getEngagementBudgetSyncTargets(payments)
824+
: [];
791825

792826
// iterate payments and build transaction list
793827
payments.forEach((payment) => {
@@ -999,6 +1033,12 @@ export class AdminService {
9991033
) {
10001034
// ideally we should be maintaining the original split of the payment amount between installments - but we aren't really using splits anymore
10011035
if (payment.installment_number === 1) {
1036+
const challengeFee = this.calculateAdjustedChallengeFee(
1037+
payment.winnings.category,
1038+
payment.challenge_markup,
1039+
body.paymentAmount,
1040+
);
1041+
10021042
transactions.push((tx) =>
10031043
this.updatePaymentAmount(
10041044
userId,
@@ -1007,6 +1047,7 @@ export class AdminService {
10071047
body.paymentAmount,
10081048
body.paymentAmount,
10091049
body.paymentAmount,
1050+
challengeFee,
10101051
version,
10111052
tx,
10121053
),
@@ -1027,6 +1068,12 @@ export class AdminService {
10271068
`update amounts -> ${body.paymentAmount.toFixed(2)} (installment 1)`,
10281069
);
10291070
} else {
1071+
const challengeFee = this.calculateAdjustedChallengeFee(
1072+
payment.winnings.category,
1073+
payment.challenge_markup,
1074+
body.paymentAmount,
1075+
);
1076+
10301077
transactions.push((tx) =>
10311078
this.updatePaymentAmount(
10321079
userId,
@@ -1035,6 +1082,7 @@ export class AdminService {
10351082
0,
10361083
0,
10371084
body.paymentAmount,
1085+
challengeFee,
10381086
version,
10391087
tx,
10401088
),
@@ -1436,6 +1484,7 @@ export class AdminService {
14361484
netAmount: number,
14371485
grossAmount: number,
14381486
totalAmount: number,
1487+
challengeFee: number | undefined,
14391488
currentVersion: number,
14401489
tx?: Prisma.TransactionClient,
14411490
) {
@@ -1459,6 +1508,7 @@ export class AdminService {
14591508
net_amount: netAmount,
14601509
gross_amount: grossAmount,
14611510
total_amount: totalAmount,
1511+
challenge_fee: challengeFee,
14621512
updated_at: new Date(),
14631513
updated_by: userId,
14641514
version: currentVersion + 1,

src/api/challenges/challenges.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ export class ChallengesService {
557557
title: challenge.name,
558558
description: payment.description || challenge.name,
559559
externalId: challenge.id,
560-
...(payment.status ? { status: payment.status } : {}),
560+
...(paymentStatus ? { status: paymentStatus } : {}),
561561
details: [
562562
{
563563
totalAmount: payment.amount,

0 commit comments

Comments
 (0)