Skip to content

Commit 2eb260e

Browse files
authored
Merge pull request #184 from topcoder-platform/PM-5043
PM-5043: Use gross challenge payments for BA sync
2 parents 7488b19 + 1f3da0c commit 2eb260e

5 files changed

Lines changed: 101 additions & 22 deletions

File tree

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,8 @@ describe('AdminService', () => {
449449
},
450450
])
451451
.mockResolvedValueOnce([
452-
{ total_amount: '12.00' },
453-
{ total_amount: '27.50' },
452+
{ gross_amount: '12.00', total_amount: '15.96' },
453+
{ gross_amount: '27.50', total_amount: '36.58' },
454454
]);
455455
topcoderChallengesService.getChallengeById.mockResolvedValue({
456456
billing: {
@@ -487,7 +487,10 @@ describe('AdminService', () => {
487487
},
488488
});
489489
expect(prisma.payment.findMany).toHaveBeenNthCalledWith(2, {
490-
select: { total_amount: true },
490+
select: {
491+
gross_amount: true,
492+
total_amount: true,
493+
},
491494
where: {
492495
billing_account: '80001012',
493496
currency: 'USD',
@@ -584,8 +587,8 @@ describe('AdminService', () => {
584587
},
585588
])
586589
.mockResolvedValueOnce([
587-
{ total_amount: '150.00' },
588-
{ total_amount: '25.00' },
590+
{ gross_amount: '150.00', total_amount: '199.50' },
591+
{ gross_amount: '25.00', total_amount: '33.25' },
589592
]);
590593
topcoderChallengesService.getChallengeById.mockResolvedValue({
591594
billing: {

src/api/admin/admin.service.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
TopcoderChallengeInfo,
3434
TopcoderChallengesService,
3535
} from 'src/shared/topcoder/challenges.service';
36+
import { resolveChallengeMemberPaymentAmount } from 'src/shared/payments/challenge-payment-amount.util';
3637
import {
3738
PaymentCycle,
3839
WinningPaymentDetailsDto,
@@ -588,20 +589,25 @@ export class AdminService {
588589
}
589590

590591
/**
591-
* Sums non-cancelled USD payment rows for a challenge and billing account.
592+
* Sums non-cancelled USD member-payment rows for a challenge and billing
593+
* account.
592594
*
593595
* @param challengeId challenge external id stored on winnings.
594596
* @param billingAccountId billing account stored on payment rows.
595-
* @returns total member-payment amount that should remain locked or consumed
596-
* for the challenge billing-account line item.
597+
* @returns Total member-payment amount that should remain locked or consumed
598+
* for the challenge billing-account line item. `gross_amount` is used before
599+
* `total_amount` so fee-inclusive totals are not marked up again.
597600
* @throws Prisma errors when the aggregate query fails.
598601
*/
599602
private async getActiveChallengePaymentTotal(
600603
challengeId: string,
601604
billingAccountId: number,
602605
): Promise<number> {
603606
const payments = await this.prisma.payment.findMany({
604-
select: { total_amount: true },
607+
select: {
608+
gross_amount: true,
609+
total_amount: true,
610+
},
605611
where: {
606612
billing_account: String(billingAccountId),
607613
currency: PrizeType.USD,
@@ -614,7 +620,12 @@ export class AdminService {
614620
});
615621
const totalAmount = payments.reduce(
616622
(sum, paymentRow) =>
617-
sum.plus(new Prisma.Decimal(paymentRow.total_amount ?? 0)),
623+
sum.plus(
624+
resolveChallengeMemberPaymentAmount({
625+
grossAmount: paymentRow.gross_amount,
626+
totalAmount: paymentRow.total_amount,
627+
}),
628+
),
618629
new Prisma.Decimal(0),
619630
);
620631

src/api/winnings/winnings.service.spec.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -519,8 +519,8 @@ describe('WinningsService', () => {
519519
status: 'COMPLETED',
520520
});
521521
tx.payment.findMany.mockResolvedValue([
522-
{ total_amount: '75.00' },
523-
{ total_amount: '25.00' },
522+
{ gross_amount: '75.00', total_amount: '99.75' },
523+
{ gross_amount: '25.00', total_amount: '33.25' },
524524
]);
525525

526526
await service.createWinningWithPayments(
@@ -546,7 +546,10 @@ describe('WinningsService', () => {
546546
);
547547

548548
expect(tx.payment.findMany).toHaveBeenCalledWith({
549-
select: { total_amount: true },
549+
select: {
550+
gross_amount: true,
551+
total_amount: true,
552+
},
550553
where: {
551554
billing_account: '80001012',
552555
currency: PrizeType.USD,
@@ -615,8 +618,8 @@ describe('WinningsService', () => {
615618
status: 'DRAFT',
616619
});
617620
tx.payment.findMany.mockResolvedValue([
618-
{ total_amount: '100.00' },
619-
{ total_amount: '50.25' },
621+
{ gross_amount: '100.00', total_amount: '120.00' },
622+
{ gross_amount: '50.25', total_amount: '60.30' },
620623
]);
621624

622625
await service.createWinningWithPayments(
@@ -645,7 +648,10 @@ describe('WinningsService', () => {
645648
'challenge-id',
646649
);
647650
expect(tx.payment.findMany).toHaveBeenCalledWith({
648-
select: { total_amount: true },
651+
select: {
652+
gross_amount: true,
653+
total_amount: true,
654+
},
649655
where: {
650656
billing_account: '80001012',
651657
currency: PrizeType.USD,

src/api/winnings/winnings.service.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
TopcoderChallengesService,
4141
} from 'src/shared/topcoder/challenges.service';
4242
import { TopcoderM2MHttpError } from 'src/shared/topcoder/topcoder-m2m.service';
43+
import { resolveChallengeMemberPaymentAmount } from 'src/shared/payments/challenge-payment-amount.util';
4344

4445
const BUDGET_LEDGER_DECIMAL_PLACES = 4;
4546
const PAYMENT_DECIMAL_PLACES = 2;
@@ -556,22 +557,25 @@ export class WinningsService {
556557
}
557558

558559
/**
559-
* Sums non-cancelled persisted USD payment rows for a challenge and billing
560-
* account.
560+
* Sums non-cancelled persisted USD member-payment rows for a challenge and
561+
* billing account.
561562
*
562563
* @param tx active Prisma transaction.
563564
* @param challengeId challenge external id stored on winnings.
564565
* @param billingAccountId billing account stored on payment rows.
565-
* @returns Payment-scale total USD amount for active challenge payments on
566-
* that billing account.
566+
* @returns Payment-scale member-payment amount for active challenge payments
567+
* on that billing account.
567568
*/
568569
private async getPersistedChallengePaymentTotal(
569570
tx: Prisma.TransactionClient,
570571
challengeId: string,
571572
billingAccountId: number,
572573
): Promise<number> {
573574
const payments = await tx.payment.findMany({
574-
select: { total_amount: true },
575+
select: {
576+
gross_amount: true,
577+
total_amount: true,
578+
},
575579
where: {
576580
billing_account: String(billingAccountId),
577581
currency: PrizeType.USD,
@@ -584,7 +588,12 @@ export class WinningsService {
584588
});
585589
const totalAmount = payments.reduce(
586590
(sum, paymentRow) =>
587-
sum.plus(new Prisma.Decimal(paymentRow.total_amount ?? 0)),
591+
sum.plus(
592+
resolveChallengeMemberPaymentAmount({
593+
grossAmount: paymentRow.gross_amount,
594+
totalAmount: paymentRow.total_amount,
595+
}),
596+
),
588597
new Prisma.Decimal(0),
589598
);
590599

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Prisma } from '@prisma/client';
2+
3+
type PaymentAmountValue = Prisma.Decimal | number | string | null | undefined;
4+
5+
export interface ChallengePaymentBudgetAmount {
6+
grossAmount?: PaymentAmountValue;
7+
totalAmount?: PaymentAmountValue;
8+
}
9+
10+
/**
11+
* Converts a persisted payment amount to a Decimal when it is finite.
12+
*
13+
* @param value Raw amount from a finance payment column.
14+
* @returns Decimal amount, or `undefined` when the value is missing or invalid.
15+
* @throws This helper does not throw for missing or non-finite values.
16+
*/
17+
function toDecimalAmount(
18+
value: PaymentAmountValue,
19+
): Prisma.Decimal | undefined {
20+
if (value === null || value === undefined) {
21+
return undefined;
22+
}
23+
24+
const amount = Number(value);
25+
26+
return Number.isFinite(amount) ? new Prisma.Decimal(amount) : undefined;
27+
}
28+
29+
/**
30+
* Resolves the member-payment amount that should be used for challenge budget sync.
31+
*
32+
* Challenge payment `total_amount` values can include the billing challenge fee
33+
* for older or external callers, while `gross_amount` is the member-payment
34+
* amount before markup. Billing-account lock/consume rows must be based on
35+
* that member amount so the markup is not applied a second time. Rows without a
36+
* valid gross amount fall back to `total_amount` for backward compatibility.
37+
*
38+
* @param payment payment amount fields from a finance payment row.
39+
* @returns Decimal member-payment amount for billing-account synchronization.
40+
* @throws This helper does not throw for invalid payment amounts.
41+
*/
42+
export function resolveChallengeMemberPaymentAmount(
43+
payment: ChallengePaymentBudgetAmount,
44+
): Prisma.Decimal {
45+
return (
46+
toDecimalAmount(payment.grossAmount) ??
47+
toDecimalAmount(payment.totalAmount) ??
48+
new Prisma.Decimal(0)
49+
);
50+
}

0 commit comments

Comments
 (0)