Unable to set subscription price to $19.99

AI-generated summary

In a bug report regarding the Discourse Subscriptions plugin, RGJ identified a floating-point precision error preventing users from setting subscription prices to $19.99. The issue stems from IEEE-754 arithmetic where parseFloat('19.99') * 100 results in 1998.9999999999998, causing the Stripe API to reject the value as an invalid integer. RGJ suggested fixing the JavaScript code in plan.js by using Math.round() to handle the conversion. pfaffman and sam discussed the root cause, noting that while storing values as cents (integers) is the standard approach (as Stripe does), the immediate fix of rounding the float conversion is less invasive. RGJ clarified that the unit_amount is passed to Stripe as cents, but the input field requires a decimal representation. Bruce5051 noted that Binary Coded Decimal (BCD) would be more suitable for currency, though not immediately applicable. The issue was acknowledged by zogstrip, who confirmed a fix was merged in pull request #37877.

  • using Discourse-Subscriptions
  • trying to add a pricing plan to a product
  • Enter 19.99 in the price box (this is already hard because the input behaves very weird)
  • Try to submit
  • Get error “Invalid integer 1998.9999999999998”

I’ve only tested this on stable 3.5.3 so far but the code is the same on beta.

Code

  @computed("unit_amount")
  get amountDollars() {
    return parseFloat(this.get("unit_amount") / 100).toFixed(2);
  }

  set amountDollars(value) {
    const decimal = parseFloat(value) * 100;
    this.set("unit_amount", decimal);
  }

Javascript / IEEE-754 float nastyness:

-> parseFloat("19.99")*100
<- 1998.9999999999998 

Suggested fix: const decimal = Math.round(Number(value) * 100)

My biggest question: why didn’t anyone notice? Didn’t anyone set the price to $19.99 before?

Oh, by the way, I also frequently get NaN, for instance when I make the input field completely empty.

2 Likes

I knew right away. One more thing I learned about computers 40+ years ago that’s not changed.

Or use 1-cent integers? I think that’s what Stripe does, actually. (But your solution requires changing much less code, so I guess it wins.)

2 Likes

Yes, that’s what it does, unit_amount are cents and the amountDollars is an ‘XX.XX’ input value.
The “invalid integer” error even comes from the Stripe API.

I guess we’re both old enough to know about the FDIV bug :wink:

3 Likes

honestly I think storing this in a float is misguided this column should be an int and stored in cents

3 Likes

It is, the parseFloat is used to convert from/to input field which is “currency with two decimals”. The unit_amount object property is obviously typeless though, hence my suggestion to use Math.round()

It is not a column server side, it is passed to the Stripe API.

1 Like

For currency BCD floating point is generally better than binary floating point.

Thanks @RGJ for the report - this should fix it :+1:

https://github.com/discourse/discourse/pull/37877

2 Likes

This topic was automatically closed after 2 days. New replies are no longer allowed.