Skip to content

Commit 2c5bcdf

Browse files
committed
PM-5043: Use gross challenge payments for BA sync
What was broken Finance rebuilt challenge billing-account lock and consume totals from payment.total_amount. Some challenge payment rows can carry a fee-inclusive total, so the billing ledger could store member payments with the challenge fee already included and then apply markup again. Root cause The previous UI and billing API fixes corrected how existing ledger rows were displayed, but the finance synchronization path could still write or rebuild challenge billing rows from fee-inclusive payment totals instead of the member-payment subtotal. What was changed Added a shared finance helper that resolves the challenge member-payment amount from gross_amount with total_amount as a fallback. Updated new winning creation sync and wallet-admin resync paths to use that member subtotal when recalculating challenge billing-account rows. Any added/updated tests Updated WinningsService and AdminService coverage so fee-inclusive total_amount rows still sync billing accounts from gross_amount member payments.
1 parent 237922f commit 2c5bcdf

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
@@ -401,8 +401,8 @@ describe('AdminService', () => {
401401
},
402402
])
403403
.mockResolvedValueOnce([
404-
{ total_amount: '12.00' },
405-
{ total_amount: '27.50' },
404+
{ gross_amount: '12.00', total_amount: '15.96' },
405+
{ gross_amount: '27.50', total_amount: '36.58' },
406406
]);
407407
topcoderChallengesService.getChallengeById.mockResolvedValue({
408408
billing: {
@@ -439,7 +439,10 @@ describe('AdminService', () => {
439439
},
440440
});
441441
expect(prisma.payment.findMany).toHaveBeenNthCalledWith(2, {
442-
select: { total_amount: true },
442+
select: {
443+
gross_amount: true,
444+
total_amount: true,
445+
},
443446
where: {
444447
billing_account: '80001012',
445448
currency: 'USD',
@@ -536,8 +539,8 @@ describe('AdminService', () => {
536539
},
537540
])
538541
.mockResolvedValueOnce([
539-
{ total_amount: '150.00' },
540-
{ total_amount: '25.00' },
542+
{ gross_amount: '150.00', total_amount: '199.50' },
543+
{ gross_amount: '25.00', total_amount: '33.25' },
541544
]);
542545
topcoderChallengesService.getChallengeById.mockResolvedValue({
543546
billing: {

src/api/admin/admin.service.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
TopcoderChallengesService,
3535
} from 'src/shared/topcoder/challenges.service';
3636
import { WinningPaymentDetailsDto } from './dto/payment-details.dto';
37+
import { resolveChallengeMemberPaymentAmount } from 'src/shared/payments/challenge-payment-amount.util';
3738

3839
const PAYMENT_DECIMAL_PLACES = 2;
3940
const BUDGET_LEDGER_DECIMAL_PLACES = 4;
@@ -573,20 +574,25 @@ export class AdminService {
573574
}
574575

575576
/**
576-
* Sums non-cancelled USD payment rows for a challenge and billing account.
577+
* Sums non-cancelled USD member-payment rows for a challenge and billing
578+
* account.
577579
*
578580
* @param challengeId challenge external id stored on winnings.
579581
* @param billingAccountId billing account stored on payment rows.
580-
* @returns total member-payment amount that should remain locked or consumed
581-
* for the challenge billing-account line item.
582+
* @returns Total member-payment amount that should remain locked or consumed
583+
* for the challenge billing-account line item. `gross_amount` is used before
584+
* `total_amount` so fee-inclusive totals are not marked up again.
582585
* @throws Prisma errors when the aggregate query fails.
583586
*/
584587
private async getActiveChallengePaymentTotal(
585588
challengeId: string,
586589
billingAccountId: number,
587590
): Promise<number> {
588591
const payments = await this.prisma.payment.findMany({
589-
select: { total_amount: true },
592+
select: {
593+
gross_amount: true,
594+
total_amount: true,
595+
},
590596
where: {
591597
billing_account: String(billingAccountId),
592598
currency: PrizeType.USD,
@@ -599,7 +605,12 @@ export class AdminService {
599605
});
600606
const totalAmount = payments.reduce(
601607
(sum, paymentRow) =>
602-
sum.plus(new Prisma.Decimal(paymentRow.total_amount ?? 0)),
608+
sum.plus(
609+
resolveChallengeMemberPaymentAmount({
610+
grossAmount: paymentRow.gross_amount,
611+
totalAmount: paymentRow.total_amount,
612+
}),
613+
),
603614
new Prisma.Decimal(0),
604615
);
605616

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -450,8 +450,8 @@ describe('WinningsService', () => {
450450
status: 'COMPLETED',
451451
});
452452
tx.payment.findMany.mockResolvedValue([
453-
{ total_amount: '75.00' },
454-
{ total_amount: '25.00' },
453+
{ gross_amount: '75.00', total_amount: '99.75' },
454+
{ gross_amount: '25.00', total_amount: '33.25' },
455455
]);
456456

457457
await service.createWinningWithPayments(
@@ -477,7 +477,10 @@ describe('WinningsService', () => {
477477
);
478478

479479
expect(tx.payment.findMany).toHaveBeenCalledWith({
480-
select: { total_amount: true },
480+
select: {
481+
gross_amount: true,
482+
total_amount: true,
483+
},
481484
where: {
482485
billing_account: '80001012',
483486
currency: PrizeType.USD,
@@ -546,8 +549,8 @@ describe('WinningsService', () => {
546549
status: 'DRAFT',
547550
});
548551
tx.payment.findMany.mockResolvedValue([
549-
{ total_amount: '100.00' },
550-
{ total_amount: '50.25' },
552+
{ gross_amount: '100.00', total_amount: '120.00' },
553+
{ gross_amount: '50.25', total_amount: '60.30' },
551554
]);
552555

553556
await service.createWinningWithPayments(
@@ -576,7 +579,10 @@ describe('WinningsService', () => {
576579
'challenge-id',
577580
);
578581
expect(tx.payment.findMany).toHaveBeenCalledWith({
579-
select: { total_amount: true },
582+
select: {
583+
gross_amount: true,
584+
total_amount: true,
585+
},
580586
where: {
581587
billing_account: '80001012',
582588
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;
@@ -539,22 +540,25 @@ export class WinningsService {
539540
}
540541

541542
/**
542-
* Sums non-cancelled persisted USD payment rows for a challenge and billing
543-
* account.
543+
* Sums non-cancelled persisted USD member-payment rows for a challenge and
544+
* billing account.
544545
*
545546
* @param tx active Prisma transaction.
546547
* @param challengeId challenge external id stored on winnings.
547548
* @param billingAccountId billing account stored on payment rows.
548-
* @returns Payment-scale total USD amount for active challenge payments on
549-
* that billing account.
549+
* @returns Payment-scale member-payment amount for active challenge payments
550+
* on that billing account.
550551
*/
551552
private async getPersistedChallengePaymentTotal(
552553
tx: Prisma.TransactionClient,
553554
challengeId: string,
554555
billingAccountId: number,
555556
): Promise<number> {
556557
const payments = await tx.payment.findMany({
557-
select: { total_amount: true },
558+
select: {
559+
gross_amount: true,
560+
total_amount: true,
561+
},
558562
where: {
559563
billing_account: String(billingAccountId),
560564
currency: PrizeType.USD,
@@ -567,7 +571,12 @@ export class WinningsService {
567571
});
568572
const totalAmount = payments.reduce(
569573
(sum, paymentRow) =>
570-
sum.plus(new Prisma.Decimal(paymentRow.total_amount ?? 0)),
574+
sum.plus(
575+
resolveChallengeMemberPaymentAmount({
576+
grossAmount: paymentRow.gross_amount,
577+
totalAmount: paymentRow.total_amount,
578+
}),
579+
),
571580
new Prisma.Decimal(0),
572581
);
573582

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)