@@ -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+ */
1526export 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