Skip to content

Commit 605c1a4

Browse files
committed
payg bundle: FE bundlePriceMinor matches the edge-fn amount_off math
bundlePriceMinor now subtracts the rounded discount (subtotal - round(subtotal*(granted-paid)/granted)) instead of rounding the discounted price, so the pre-mint estimate matches create-payg-bundle-quote's inline amount_off coupon to the penny on exact-half ties. Same results as before except tie cases; adds a tie-case test that pins the method.
1 parent 3813ca3 commit 605c1a4

2 files changed

Lines changed: 25 additions & 11 deletions

File tree

frontend/editor/src/proprietary/billing/format.test.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ describe("prepaid bundle brain", () => {
5454
expect(q.provisionedMonthlyVolume).toBe(10000);
5555
expect(q.poolCredits).toBe(576_000);
5656
expect(q.listMinor).toBe(576_000); // 576k credits × 1¢
57-
expect(q.priceMinor).toBe(480_000); // × 10/12
57+
expect(q.priceMinor).toBe(480_000); // 576,000 − round(576000×2/12)=96,000
5858
expect(q.savingsMinor).toBe(96_000);
5959
expect(q.overEnterprise).toBe(false);
6060
});
@@ -86,17 +86,26 @@ describe("prepaid bundle brain", () => {
8686
});
8787

8888
it("applies the 12-for-10 discount at the per-run rate", () => {
89-
// 120k credits × 2 minor = 240,000 list; × 10/12 = 200,000 paid.
89+
// 120k credits × 2 minor = 240,000 list; minus round(240000×2/12)=40,000 = 200,000 paid.
9090
expect(bundleListMinor(120_000, 2)).toBe(240_000);
9191
expect(bundlePriceMinor(120_000, 2)).toBe(200_000);
9292
});
9393

9494
it("rounds sub-cent rates to the minor unit (HALF_UP)", () => {
95-
// 100k × 0.5 = 50,000 list; × 10/12 = 41,666.67 → 41,667.
95+
// 100k × 0.5 = 50,000 list; minus round(50000×2/12)=round(8333.33)=8,333 = 41,667.
9696
expect(bundleListMinor(100_000, 0.5)).toBe(50_000);
9797
expect(bundlePriceMinor(100_000, 0.5)).toBe(41_667);
9898
});
9999

100+
it("rounds the discount then subtracts, matching the edge fn on exact-half ties", () => {
101+
// The mechanism that kills the coupon-rounding drift: round the DISCOUNT and subtract it, not
102+
// round the discounted price. On a tie the two differ by a minor unit.
103+
// subtotal 3 → discount round(0.5)=1 → 2 (round(3×10/12)=round(2.5)=3 would drift).
104+
expect(bundlePriceMinor(3, 1)).toBe(2);
105+
// subtotal 9 → discount round(1.5)=2 → 7 (round(9×10/12)=round(7.5)=8 would drift).
106+
expect(bundlePriceMinor(9, 1)).toBe(7);
107+
});
108+
100109
it("returns null money when the rate is unknown or non-positive", () => {
101110
expect(bundleListMinor(120_000, null)).toBeNull();
102111
expect(bundlePriceMinor(120_000, null)).toBeNull();

frontend/editor/src/proprietary/billing/format.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,21 +199,26 @@ export function bundleListMinor(
199199
}
200200

201201
/**
202-
* Discounted price of a prepaid pool in minor units: units × rate × paid/granted.
203-
* Mirror of the Stripe coupon. Null when the rate is unknown (the caller hides the
204-
* figure and falls back to the server total).
202+
* Discounted price of a prepaid pool in minor units: the list subtotal minus the
203+
* rounded 12-for-10 discount. Computed the SAME way as the edge fn that mints the
204+
* Stripe coupon (create-payg-bundle-quote: round the DISCOUNT, then subtract it —
205+
* not round the discounted price), so this pre-mint estimate matches the amount_off
206+
* Stripe charges, and the total persisted on the quote, to the penny. The two methods
207+
* diverge by a minor unit on exact-half ties. Null when the rate is unknown (the
208+
* caller hides the figure and falls back to the server total).
205209
*/
206210
export function bundlePriceMinor(
207211
units: number,
208212
ratePerUnitMinor: number | null | undefined,
209213
monthsPaid: number = PREPAID_MONTHS_PAID,
210214
monthsGranted: number = PREPAID_MONTHS_GRANTED,
211215
): number | null {
212-
const rate =
213-
ratePerUnitMinor != null && ratePerUnitMinor > 0 ? ratePerUnitMinor : null;
214-
return rate != null
215-
? Math.round((units * rate * monthsPaid) / monthsGranted)
216-
: null;
216+
const subtotal = bundleListMinor(units, ratePerUnitMinor);
217+
if (subtotal == null) return null;
218+
const discount = Math.round(
219+
(subtotal * (monthsGranted - monthsPaid)) / monthsGranted,
220+
);
221+
return subtotal - discount;
217222
}
218223

219224
/** Inputs to {@link computeBundleQuote} — team size + the finer-setting multipliers. */

0 commit comments

Comments
 (0)