Line item discount incorrect rounding in POS

I have a line item (POS App) with quantity 3 and price £4.00, total = £12.00

I apply a line item discount (fixed amount) of £4.00.

However, the resulting price to pay should be £8.00, but it is actually £8.01

I understand how the calculation done, why it is wrong and why we need a different line items discount implementation.

The mechanics:

When you apply a (fixed) discount of £4.00, it divides that by the quantity of items, in this case 3 and comes up with £1.33 (rounded from 1.33333333333). Then multiplies that by the quantity to come up with £3.99 - hence the balance to pay is £8.01, not £8.00 as it should be.

Clearly this cannot be fixed with the current implementation. What we need is “real” “fixed amount” discounts, that are never converted to percentages and never change even when quantity changes.

There is another related issue as well:

Take a scenario - customer is buying 3 items, but has a voucher for 1 free. So I add 3 items to the cart, then apply a discount for the price of 1 item. We get the nasty rounding error above which is bad enough. But it gets worse… If then customer then decided they will take only 2, or maybe 4 items, then we change the quantity which causes the discount amount to change - since POS has converted our “fixed” amount to a percentage and then multiplies that percentage by the quantity.

So my £3.99 total “fixed” discount becomes £2.66 (2 items) or £5.32 (4 items) and no longer reflects “1 free items with the coupon”.

But it gets worse again… let’s say some other app has also applied a discount to this line item. And I decided the customers coupon does not apply, so I remove my line item discount. Except that I can’t. I can only remove ALL discounts from this line item. So I remove the other discount applied by the other app.

I can re-apply any other discounts I see in the line item. But that relies on me coding that in. What if this is reversed and some other app removes my discount?

So I would very much like line items discounts to be re-thought. Keep the existing implementation as the default, but add the option to apply a “this is REALLY fixed” discount that never changes. And add the option to remove an individual line item discount rather than all discounts.

Hi Ian - thank you for this feedback, it’s helpful.
Our decision here was intentional to drive consistency across discount experiences in POS and online store.
Automatic or code-based discounts can often meet many merchant needs including some of the scenarios you outline above.

Thanks for the response.

Firstly, despite my frustration at a lack of flexibility in certain areas, I am astonished how great a plug-in system you have built to support developers. It is truly impressive! And I very much understand the need to maintain consistency and to impose limits.

That said, consistency is great, but constantly wrong is not so great…

The problem is that “FixedAmount” does not do what it implies it does, and there is no way to actually apply a "fixed’ discount to a line item. The conversion to % with rounding then multiplying back out means that discount applied is not the one requested or required.

This is a problem that will consistency mean staff will need to apply manual 1p/2p type discounts to correct the “FixedAmount” rounding errors - or deal with customers querying why they have been charged 1p for FREE coffee…

Sadly, we can only correct if the calculated discount is too low. We cannot correct when the calculation produced too much of a discount since negative discounts are not permitted.

The scenario I mentioned is a real world one that Shopify POS simply does not support (or not, without rounding errors). It really would be nice if Shopify accepted that discounts can be applied to a limited number of items, not the full purchase quantity.

If we could have a separate line item for discount items to non-discounted items, that would suffice as @tod97 mentioned here : Shopify POS UI Extension: `shopify.cart.addLineItem` Performance Bottleneck - #10 by tod97 that would enable a workaround. Unfortunately, POS aggressively combines variantIds and not permit this.

As for why my customers want an alternative discount / loyalty scheme - is simply because your automatic / coded discounts do not meet their needs. Sometimes a variable discount is needed (redeeming a variable number of loyalty points for a variable discount). Or redeeming a number of full stamp cards for a number of free coffees (not making ALL coffee purchased free!).

Hey @Ian_Bale ,

I posted a longer, more detailed reply to this in this thread here. But I think the rounding bug has one root cause: when a FixedAmount discount sits on a line with quantity > 1, POS divides the discount by the quantity, rounds per unit, then multiplies back. That’s where your £2.99 becomes £3.00.

I had an idea for a fix for you, which is to make sure the free coffee is the only thing on its line - a quantity-1 line. On a quantity-1 line there’s nothing to divide, so the rounding never happens. A Percentage 100% discount is cleanest (100% of £2.99 = £2.99 exactly, and it stays correct if the coffee price ever changes), though a £2.99 FixedAmount works equally well once the line is quantity 1.

The catch is that 4 coffees of the same variant added with no properties are merged into a single line of quantity 4 - so “pick one” actually means splitting one unit off. A line item property does exactly that, and does double duty: it forces the split and stops the free line from re-merging into the paid one:

const cart = shopify.cart.current.value;
const targetUuid = '<uuid of the coffee line>';

// Split one unit off the coffee line onto its own quantity-1 line.
const newLineItems = cart.lineItems.reduce((list, row) => {
  if (row.uuid === targetUuid && row.quantity > 1) {
    return [
      ...list,
      // the paid coffees stay together
      { ...row, quantity: row.quantity - 1 },
      // the free one — the property both splits it off and prevents a re-merge
      { variantId: row.variantId, quantity: 1, properties: { Stampcard: 'Redeemed' } },
    ];
  }
  return [...list, row];
}, []);

const updatedCart = await shopify.cart.bulkCartUpdate({
  lineItems: newLineItems,
  note: cart.note,
  customer: cart.customer,
});

// Find the freshly split line by its property and zero it out.
const freeLine = updatedCart.lineItems.find(
  (li) => li.properties?.Stampcard === 'Redeemed',
);

await shopify.cart.setLineItemDiscount(
  freeLine.uuid,
  'Percentage',
  'Stampcard reward',
  '100',
);

This also fixes your second bug - adding a 5th coffee now merges into the paid line, not the discounted one, so the reward can’t creep up to £4.

(If you’d rather have this happen automatically server-side, or need identical behaviour across online + POS, a Cart Transform LineExpand function can do the split for you - but for a staff-driven redemption at the till, the above is all you need.)

I hope this is helpful, but please disregard if this is not what you’re looking for :slight_smile:
Adam

Hi @adamwooding

I found your suggestion on your issue page. It’s is definitely helpful and expands my limited POS knowledge (my first project!).

However I found another workaround. Rather than use FixedAmount, I’ve calculated a Percentage (100/quantity*discounted quantity) - which appears to use sufficient decimal places to come up with the correct rounding. This avoids that initial conversion and rounding on a per item basis which plagues “FixedAmount” and seems to generate the correct discount. It’s fairly trivial to adjust the percentage if quantities change.

I do think the “FixedAmount” is rather misleading, and given the rounding problem, is clearly not what could be reasonably expected. A true, “fixed amount” would certainly be useful and would reduce cart change churn since no further changes would need to be applied when quantities changed. On a related note, it would be helpful if line items with percentage discounts had a property of the percentage amount rather than just the calculated discount!

Hi @Ian_Bale,

Thanks so much for your reply :slight_smile:

The percentage workaround you’ve found is fantastic - I think for your exact use case it’s actually a better fit than what I suggested.

Please see the following with regards to each of your points & questions:

Your percentage workaround

100 / quantity * discountedQuantity should the right formula - my understanding is that Shopify POS divides the percentage before allocating per unit, so the £2.99 ÷ 3, round, × 3 rounding cascade that affects FixedAmount never happens. For 4 coffees with 1 free → 25%, 25% of £11.96 = £2.99 exactly, which is much cleaner.

Split-row UX concern

I do agree with you about your concerns with using split rows for this. Once you have a quantity-3 paid line and a quantity-1 free line, the cashier can change either quantity independently - and as you say, they could push the free line above the redeemable stamp count without realising. The percentage approach keeps everything on one line, which is the natural UX for “X out of Y of these are free.” Line splitting is realy good for intrinsic per-item differences (monograms, allergen flags, “no cream”) rather than “this one’s free” situation.

The one tradeoff you’ve already mentioned: when the cashier changes the quantity, the percentage has to be recalculated. That means subscribing to cart changes and reapplying.

It’s churn, but it’s predictable churn - and arguably easier to reason about than defending a split row from quantity edits.

On the timing question

Yes - I think your suspicion about the earlier attempt is right. The sequencing has to be:

const uuidA = await shopify.cart.addLineItem(variantId, 1);
await shopify.cart.addLineItemProperties(uuidA, { … });  // must finish first
const uuidB = await shopify.cart.addLineItem(variantId, 1);

Without the await in between, both adds can land at POS before the property write does, and they get merged into a single quantity-2 line - the property eventually arrives but the split has already been lost. Awaiting each step in order what you need.

On bulkCartUpdate and change events

I presume the bulk update would result in just one change event?

That’s my understanding of the docs too - it’s described as a single bulk operation that returns the updated cart once everything has been applied. I’d expect one consolidated change-event emission rather than one per field touched. Worth a 30-second sanity check on your end with a shopify.cart.current.subscribe(c => console.log('tick', c)) while you fire the bulk update - but the design intent clearly points that way, and it’s exactly the reason to prefer it over your current lineItemProperty + cartProperty + cartDiscount sequence.

For your specific 3-event pattern, bulkCartUpdate handles all three of those in a single input shape:

await shopify.cart.bulkCartUpdate({
  lineItems: rebuiltLineItems,           // includes the line-item-property change
  properties: { …updatedCartProperties }, // cart-level properties
  cartDiscount: { …yourDiscount },        // cart-level discount
  note: currentCart.note,
  customer: currentCart.customer,
});

So even if you’re already doing a merge of mutations, that is the next rung down.

Re-applying line-item discounts after a bulk update

This is a good question, and definitely worth confirming. The docs describe LineItem discounts as read-only output on the cart - there isn’t a documented field for handing discounts back in via the LineItem[] you pass to bulkCartUpdate. So while you could try it, I wouldn’t rely on it surviving a future SDK change.

The pattern I’d use instead is to chain a bulkSetLineItemDiscounts call right after, which batches every line-item discount into a single mutation:

const updatedCart = await shopify.cart.bulkCartUpdate({
  lineItems: rebuiltLineItems,
  note: currentCart.note,
  customer: currentCart.customer,
});

// Re-apply (or freshly apply) line-item discounts in one batched call.
await shopify.cart.bulkSetLineItemDiscounts([
  {
    lineItemUuid: someUuid,
    lineItemDiscount: { type: 'Percentage', title: 'Stampcard reward', amount: '100' },
  },
  // …more entries if you have them
]);

Two mutations total - but each is its own bulk operation, so you should see two change-events instead of the 3+ you currently get, and the line-item-discount step covers all discounts at once rather than one per call.

On FixedAmount and percentage-as-property

I definitely agree on both. A truly fixed FixedAmount (no per-unit re-allocation) would eliminate exactly the cart churn you’re trying to escape, and exposing the percentage value itself on the line item - not just the calculated money amount - would make quantity-reactive logic dramatically simpler.

Hope that’s useful - and nice find on the percentage formula. I’d honestly recommend that as the primary approach for the stampcard flow over what I suggested.

Thanks so much Ian,
Adam :slight_smile:

Thanks @adamwooding

I am going to give the bulk update a try out. But not for a while. More pressing matters to deal with first. I’l report back here since it might be useful for someone else anyway.

I see you are still waiting on Shopify to deal with the insertLineItem latency…

I did get an email from them saying my request for multiple cart level discounts has been sent as an official request to the dev team. So good news there!