Skip to content

Commit a2ea0aa

Browse files
committed
chore: have tests describe limits of rounding behavior
OTPs require application_fee_amounts so we calc them using a helper. However this means that for really small purchases, we charge nothing. We explicitly document this
1 parent 0c9059c commit a2ea0aa

1 file changed

Lines changed: 22 additions & 2 deletions

File tree

apps/backend/src/lib/payments/platform-fees.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ export function getApplicationFeeBps(projectId: string): number {
1212
return APPLICATION_FEE_BPS;
1313
}
1414

15+
/**
16+
* Half-to-nearest rounding. Stripe's `application_fee_amount` is an integer
17+
* in stripe-units, so we can't represent 0.9% exactly when the charge isn't
18+
* a multiple of $10. Round-nearest is unbiased on average — over many
19+
* charges the over- and under-rounding cancel — at the cost of producing a
20+
* 0 fee on charges in Stripe's min-charge band ($0.50–$0.55) where 0.9%
21+
* falls below half a cent. That clip-to-zero band is small enough to be
22+
* acceptable lost revenue; the alternative (ceil) over-collects on every
23+
* non-multiple-of-$10 charge, and a fractional-cents ledger is more
24+
* complexity than the precision is worth here.
25+
*/
1526
export function computeApplicationFeeAmount(options: { amountStripeUnits: number, projectId: string }): number {
1627
const bps = getApplicationFeeBps(options.projectId);
1728
if (bps === 0) return 0;
@@ -32,12 +43,21 @@ import.meta.vitest?.describe("platform fee helpers", (test) => {
3243
expect(getApplicationFeeBps("proj_abc123")).toBe(APPLICATION_FEE_BPS);
3344
expect(getApplicationFeeBps("some-uuid")).toBe(APPLICATION_FEE_BPS);
3445
});
35-
test("computeApplicationFeeAmount is 0.9% of the charge, rounded", ({ expect }) => {
46+
test("computeApplicationFeeAmount is 0.9% of the charge, rounded half-to-nearest", ({ expect }) => {
3647
expect(computeApplicationFeeAmount({ amountStripeUnits: 10000, projectId: "p" })).toBe(90);
3748
expect(computeApplicationFeeAmount({ amountStripeUnits: 12345, projectId: "p" })).toBe(111);
3849
expect(computeApplicationFeeAmount({ amountStripeUnits: 500000, projectId: "p" })).toBe(4500);
3950
});
40-
test("computeApplicationFeeAmount is 0 for internal project", ({ expect }) => {
51+
test("computeApplicationFeeAmount clips to 0 below the half-cent threshold (~$0.56)", ({ expect }) => {
52+
// Documented tradeoff: charges in Stripe's min-charge band whose 0.9%
53+
// is under half a cent round to a 0 fee. Pinned here so a future reader
54+
// doesn't accidentally "fix" the clipping without weighing the
55+
// alternatives (see the JSDoc on computeApplicationFeeAmount).
56+
expect(computeApplicationFeeAmount({ amountStripeUnits: 50, projectId: "p" })).toBe(0);
57+
expect(computeApplicationFeeAmount({ amountStripeUnits: 55, projectId: "p" })).toBe(0);
58+
expect(computeApplicationFeeAmount({ amountStripeUnits: 56, projectId: "p" })).toBe(1);
59+
});
60+
test("computeApplicationFeeAmount is 0 for internal project even on large charges", ({ expect }) => {
4161
expect(computeApplicationFeeAmount({ amountStripeUnits: 10000, projectId: "internal" })).toBe(0);
4262
});
4363
test("getApplicationFeePercentOrUndefined returns 0.9 for non-internal", ({ expect }) => {

0 commit comments

Comments
 (0)