Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions packages/finance-prisma-client/edge.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions packages/finance-prisma-client/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/finance-prisma-client/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "prisma-client-8e6584db420344c104dfb4ed4bc1d325cd5765efdf218c5bc263260e360dc646",
"name": "prisma-client-1c9f61cf8e792f6948abaf67485be8959510b59ae7726d3b9d635f251ec9c9fb",
"main": "index.js",
"types": "index.d.ts",
"browser": "default.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/finance-prisma-client/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ model payment {
release_date DateTime? @default(dbgenerated("(CURRENT_TIMESTAMP + '15 days'::interval)")) @db.Timestamp(6)
payment_status payment_status?
billing_account String @db.VarChar(80)
challenge_markup Decimal? @db.Decimal(12, 2)
challenge_markup Decimal? @db.Decimal(12, 4)
challenge_fee Decimal? @db.Decimal(12, 2)
payment_method payment_method? @relation(fields: [payment_method_id], references: [payment_method_id], onDelete: NoAction, onUpdate: NoAction)
winnings winnings @relation(fields: [winnings_id], references: [winning_id], onDelete: NoAction, onUpdate: NoAction)
Expand Down
5 changes: 2 additions & 3 deletions packages/finance-prisma-client/wasm.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "payment" ALTER COLUMN "challenge_markup" TYPE DECIMAL(12,4);
2 changes: 1 addition & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ model payment {
release_date DateTime? @default(dbgenerated("(CURRENT_TIMESTAMP + '15 days'::interval)")) @db.Timestamp(6)
payment_status payment_status?
billing_account String @db.VarChar(80)
challenge_markup Decimal? @db.Decimal(12, 2)
challenge_markup Decimal? @db.Decimal(12, 4)
challenge_fee Decimal? @db.Decimal(12, 2)
payment_method payment_method? @relation(fields: [payment_method_id], references: [payment_method_id], onDelete: NoAction, onUpdate: NoAction)
winnings winnings @relation(fields: [winnings_id], references: [winning_id], onDelete: NoAction, onUpdate: NoAction)
Expand Down
10 changes: 5 additions & 5 deletions src/api/winnings/winnings.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,10 @@ describe('WinningsService', () => {
]);
});

it('rounds engagement challenge markup before persisting the payment fee', async () => {
it('preserves engagement challenge markup precision before calculating the payment fee', async () => {
billingAccountsService.getBillingAccountById.mockResolvedValue({
id: 123456,
markup: 0.236,
markup: 0.3989,
});

await service.createWinningWithPayments(
Expand Down Expand Up @@ -243,12 +243,12 @@ describe('WinningsService', () => {
const persistedPayment =
tx.winnings.create.mock.calls[0][0].data.payment.create[0];

expect(Number(persistedPayment.challenge_markup)).toBe(0.24);
expect(Number(persistedPayment.challenge_fee)).toBe(24);
expect(Number(persistedPayment.challenge_markup)).toBe(0.3989);
expect(Number(persistedPayment.challenge_fee)).toBe(39.89);
expect(billingAccountsService.consumeAmounts).toHaveBeenCalledWith({
consumes: [
{
amount: 124,
amount: 139.89,
billingAccountId: 123456,
externalId: 'assignment-1',
externalType: 'ENGAGEMENT',
Expand Down
37 changes: 25 additions & 12 deletions src/api/winnings/winnings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { resolveChallengeMemberPaymentAmount } from 'src/shared/payments/challen

const BUDGET_LEDGER_DECIMAL_PLACES = 4;
const PAYMENT_DECIMAL_PLACES = 2;
const PAYMENT_MARKUP_DECIMAL_PLACES = 4;
export const CHALLENGE_BUDGET_SYNC_SKIP_ATTRIBUTE = 'skipChallengeBudgetSync';

interface EngagementBillingAccountConsume {
Expand Down Expand Up @@ -729,11 +730,25 @@ export class WinningsService {
}

/**
* Normalizes the engagement billing-account markup to the payment column
* scale used by `payment.challenge_markup`.
* Quantizes a payment markup to the persisted challenge-markup scale.
*
* Finance stores `payment.challenge_markup` at the same four-decimal scale as
* billing-accounts-api-v6 markup values so fee calculations do not round the
* rate to cents before multiplication.
*
* @param markup Decimal markup to normalize.
* @returns JavaScript number rounded to the challenge-markup column scale.
*/
private toPaymentMarkup(markup: Prisma.Decimal): number {
return this.toScaledAmount(markup, PAYMENT_MARKUP_DECIMAL_PLACES);
}

/**
* Normalizes the engagement billing-account markup for payment persistence
* while preserving billing-account precision for downstream fee math.
*
* @param markup billing-account markup returned by billing-accounts-api-v6.
* @returns Rounded markup suitable for persistence on payment rows.
* @returns Four-decimal markup suitable for persistence on payment rows.
* @throws InternalServerErrorException when the markup is not finite.
*/
private calculateEngagementChallengeMarkup(markup: number): number {
Expand All @@ -743,16 +758,15 @@ export class WinningsService {
);
}

return this.toPaymentAmount(new Prisma.Decimal(markup));
return this.toPaymentMarkup(new Prisma.Decimal(markup));
}

/**
* Computes the persisted engagement challenge fee using the rounded payment
* markup scale.
* Computes the persisted engagement challenge fee using the normalized
* billing-account markup.
*
* @param totalAmount payment detail total amount.
* @param challengeMarkup rounded billing-account markup persisted on the
* payment row.
* @param challengeMarkup billing-account markup persisted on the payment row.
* @param detailIndex zero-based detail index for error reporting.
* @returns Payment-scale challenge fee.
* @throws BadRequestException when the detail amount is invalid.
Expand Down Expand Up @@ -793,8 +807,7 @@ export class WinningsService {
* Computes an engagement consume amount with decimal-safe arithmetic.
*
* @param totalAmount payment detail total amount.
* @param challengeMarkup rounded billing-account markup persisted on the
* payment row.
* @param challengeMarkup billing-account markup persisted on the payment row.
* @param detailIndex zero-based detail index for error reporting.
* @returns Ledger-scale consume amount including markup.
* @throws BadRequestException when the detail amount is invalid.
Expand Down Expand Up @@ -852,8 +865,8 @@ export class WinningsService {
*
* @param body incoming winning creation request.
* @returns assignment id, trusted billing account id, and typed consume
* requests to execute for non-TopGear billing accounts, plus the rounded
* challenge markup persisted on the created payment rows.
* requests to execute for non-TopGear billing accounts, plus the normalized
* four-decimal challenge markup persisted on the created payment rows.
* @throws BadRequestException when engagement payment input is invalid.
* @throws InternalServerErrorException when billing-account metadata cannot
* be normalized into a finite markup.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
--
-- Calculation:
-- - payment.challenge_markup = billing account markup, rounded to the
-- payment column scale.
-- challenge-markup column scale.
-- - payment.challenge_fee = payment.total_amount * payment.challenge_markup.
-- - "billing-accounts"."ConsumedAmount".amount = payment.total_amount +
-- payment.challenge_fee, rounded to the billing ledger scale.
Expand Down Expand Up @@ -75,10 +75,10 @@ WITH calculated AS (
p.winnings_id,
w.external_id AS "assignmentId",
p.billing_account AS "billingAccount",
ROUND(ba.markup::numeric, 2) AS "challengeMarkup",
ROUND(ba.markup::numeric, 4) AS "challengeMarkup",
CASE
WHEN p.total_amount IS NULL THEN NULL
ELSE ROUND(p.total_amount * ROUND(ba.markup::numeric, 2), 2)
ELSE ROUND(p.total_amount * ROUND(ba.markup::numeric, 4), 2)
END AS "challengeFee",
p.challenge_markup AS "currentChallengeMarkup",
p.challenge_fee AS "currentChallengeFee"
Expand Down Expand Up @@ -106,10 +106,10 @@ FROM candidates;
WITH calculated AS (
SELECT
p.payment_id,
ROUND(ba.markup::numeric, 2) AS "challengeMarkup",
ROUND(ba.markup::numeric, 4) AS "challengeMarkup",
CASE
WHEN p.total_amount IS NULL THEN NULL
ELSE ROUND(p.total_amount * ROUND(ba.markup::numeric, 2), 2)
ELSE ROUND(p.total_amount * ROUND(ba.markup::numeric, 4), 2)
END AS "challengeFee"
FROM "finance"."payment" p
INNER JOIN "finance"."winnings" w
Expand Down Expand Up @@ -167,7 +167,7 @@ WITH finance_payment_rows AS (
ROUND(
(
p.total_amount
+ ROUND(p.total_amount * ROUND(ba.markup::numeric, 2), 2)
+ ROUND(p.total_amount * ROUND(ba.markup::numeric, 4), 2)
)::numeric,
4
) AS "expectedConsumedAmount"
Expand Down Expand Up @@ -242,7 +242,7 @@ WITH finance_payment_rows AS (
ROUND(
(
p.total_amount
+ ROUND(p.total_amount * ROUND(ba.markup::numeric, 2), 2)
+ ROUND(p.total_amount * ROUND(ba.markup::numeric, 4), 2)
)::numeric,
4
) AS "expectedConsumedAmount"
Expand Down
10 changes: 5 additions & 5 deletions src/scripts/backfill-payment-challenge-markup-fees.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
--
-- Calculation:
-- - payment.challenge_markup = challenges.ChallengeBilling.markup,
-- rounded to the payment column scale.
-- rounded to the challenge-markup column scale.
-- - payment.challenge_fee = payment.challenge_markup * payment.total_amount
--
-- Run this against the PostgreSQL database that contains these schemas:
Expand Down Expand Up @@ -39,10 +39,10 @@ WITH calculated AS (
p.payment_id,
p.winnings_id,
w.external_id AS "challengeId",
ROUND(cb."markup"::numeric, 2) AS "challengeMarkup",
ROUND(cb."markup"::numeric, 4) AS "challengeMarkup",
CASE
WHEN p.total_amount IS NULL THEN NULL
ELSE ROUND(p.total_amount * ROUND(cb."markup"::numeric, 2), 2)
ELSE ROUND(p.total_amount * ROUND(cb."markup"::numeric, 4), 2)
END AS "challengeFee",
p.challenge_markup AS "currentChallengeMarkup",
p.challenge_fee AS "currentChallengeFee"
Expand All @@ -69,10 +69,10 @@ FROM candidates;
WITH calculated AS (
SELECT
p.payment_id,
ROUND(cb."markup"::numeric, 2) AS "challengeMarkup",
ROUND(cb."markup"::numeric, 4) AS "challengeMarkup",
CASE
WHEN p.total_amount IS NULL THEN NULL
ELSE ROUND(p.total_amount * ROUND(cb."markup"::numeric, 2), 2)
ELSE ROUND(p.total_amount * ROUND(cb."markup"::numeric, 4), 2)
END AS "challengeFee"
FROM "finance"."payment" p
INNER JOIN "finance"."winnings" w
Expand Down
Loading