1

I am calculating subtotal, tax, shipping and insurance based on the following:

public getSubTotal() {
  this.subTotal =
    document.getElementById("total").value -
    (document.getElementById("total").value * 0.07 +
      document.getElementById("total").value * 0.03 +
      document.getElementById("total").value * 0.01 +
      1.0);
  document.getElementById("tax").value =
    document.getElementById("total").value * 0.07;
  document.getElementById("shipping").value =
    document.getElementById("total").value * 0.03;
  document.getElementById("insurance").value =
    document.getElementById("total").value * 0.01;
  console.log(
    "tax: " +
      document.getElementById("total").value * 0.07 +
      " shipping: " +
      document.getElementById("total").value * 0.03 +
      " ins: " +
      document.getElementById("total").value * 0.01 +
      " total: " +
      document.getElementById("total").value +
      " subtotal: " +
      this.subTotal
  );
  return this.subTotal.toFixed(2);
}

details: {
  subtotal: document.getElementById("subTotal").value,
  tax: parseFloat(document.getElementById("tax").value).toFixed(
    2
  ),
  shipping: parseFloat(
    document.getElementById("shipping").value
  ).toFixed(2),
  handling_fee: "1.00",
  shipping_discount: "0.00",
  insurance: parseFloat(
    document.getElementById("insurance").value
  ).toFixed(2)
}

The total is fixed with each product.

When I post this to PayPal API, the numbers sometimes add up and sometimes they do not. I am loss for how to fix it.

The following is an example of what is posted and I do not understand why PayPal is rejecting this example:

details {…}
handling_fee    1.00
insurance   0.23
shipping    0.68
shipping_discount   0.00
subtotal    19.30
tax 1.60
total   22.81

All of these numbers were based of 22.81 and the console shows:

tax: 1.5967 
shipping: 0.6842999999999999 
ins: 0.2281 
total: 22.81 
subtotal: 19.3009

How can I keep parseFloat().toFixed(2) from changing the numbers? And if I leave the numbers with having more than two decimal place number then PayPal rejects it for being malformatted.

When I use a calculator on these numbers, I receive a result of 22.81

The following is the error that I receive from PayPal - if I comment out the details section, then the post goes through without errors:

Error: Request to post https://www.sandbox.paypal.com/v1/payments/payment failed with 400 error. Correlation id: 12d500c6247f1, 12d500c6247f1

{
    "name": "VALIDATION_ERROR",
    "details": [
        {
            "field": "transactions[0]",
            "issue": "Item amount must add up to specified amount subtotal (or total if amount details not specified)"
        }
    ],
    "message": "Invalid request - see details",
    "information_link": "https://developer.paypal.com/docs/api/payments/#errors",
    "debug_id": "12d500c6247f1"
}

The following are a couple of screen shots: you can see the hidden inputs have the correct values you can see the data posted to PayPal

For sure, as the amount of products increase, so does the difference in what is added, along with the total given, which leads to my overall question.

How should I go about calculating these items correctly?

Thanks in advance

kronus
  • 902
  • 2
  • 16
  • 34
  • 1
    The `.toFixed()` function returns a **string**, not a number. Adding strings means string concatenation, not numeric addition. You cannot constrain JavaScript numbers to a fixed number of decimal places because JavaScript numbers are *binary* floating-point values. – Pointy Jan 17 '19 at 00:20
  • @Pointy thank you for replying. That is why I use parseFloat().toFixed(2) – kronus Jan 17 '19 at 00:25
  • Right, but after calling `.toFixed()` if you make it a number again you go right back to binary floating-point. "Money math" in JavaScript (or any other language using binary floating-point) just does not work reliably because many decimal fractions cannot be represented exactly. – Pointy Jan 17 '19 at 00:27
  • @Pointy excellent, so what should I do to calculate the numbers correctly with only two decimal points – kronus Jan 17 '19 at 00:49
  • Multiply input by 100, and do the arithmetic with that. When you want to show the results, divide by 100 and use `toFixed(2)`. – Barmar Jan 17 '19 at 01:32
  • Basically, in any programming language (not just javascript) you should never use floating point numbers for monetary calculations. The standard in accounting software is to use integers and calculate cents instead of dollars. You convert to dollars ONLY for display purposes – slebetman Jan 17 '19 at 03:37

2 Answers2

1

When you are working with money values you can avoid many rounding errors by working in cents, have all your costs in cents and when you display them you can display

(cost/100).toFixed(2)

This will use integer math over floating point and you will get way less rounding error.

Adrian Brand
  • 20,384
  • 4
  • 39
  • 60
0

After going round and round with taking the price of the product, then subtracting the amount of tax, shipping, handling and insurance, to reach the subtotal - it dawned on me that I was doing this backgrounds and, because of float points being somewhat "unpredictable," then the amounts were never going to match 100% of the time.

That's when it hit me and it was so obvious. I should be adding all those numbers to the price of the product:

 getFinalAmount() {
  this.model.subTotal = this.subTotal = this.prdSrvc.getPriceTotal();
  this.subTotal = parseFloat(this.subTotal).toFixed(2);

  document.getElementById("tax").value = this.model.tax = this.tax =
    this.subTotal * 0.07;
  document.getElementById(
    "shipping"
  ).value = this.model.shipping = this.shipping = this.subTotal * 0.03;
  document.getElementById(
    "insurance"
  ).value = this.model.insurance = this.insurance = this.subTotal * 0.01;

  this.total = this.payPalSrvc.finalAmount =
    parseFloat(this.tax) +
    parseFloat(this.shipping) +
    parseFloat(this.insurance) +
    parseFloat(this.subTotal) +
    1.0;

  this.payPalSrvc.finalAmount = parseFloat(
    this.payPalSrvc.finalAmount
  ).toFixed(2);
  this.total = parseFloat(this.total).toFixed(2);

  document.getElementById(
    "subTotal"
  ).value = this.model.subTotal = this.subTotal;
  document.getElementById("total").value = this.model.total = this.total;

  this.tax = parseFloat(this.tax).toFixed(2);
  this.shipping = parseFloat(this.shipping).toFixed(2);
  this.insurance = parseFloat(this.insurance).toFixed(2);
  return this.payPalSrvc.finalAmount;
}

I also removed the currency pipe from the html, leaving me with straight numbers with two decimal points:

    <input type="hidden" name="tax" id="tax" value="{{ this.tax }}" />
    <input
      type="hidden"
      name="shipping"
      id="shipping"
      value="{{ this.shipping }}"
    />
    <input
      type="hidden"
      name="insurance"
      id="insurance"
      value="{{ this.insurance }}"
    />

The following is what the order looks like in the paypal dashboard: paypal dashboard detail of order

kronus
  • 902
  • 2
  • 16
  • 34