← All blogs

Product engineering

POS partial refunds: who gets the last cent?

A customer brings back the mug from a USD 30 basket. They paid USD 20 after a USD 10 discount. What do you give them back? If checkout saved only the basket total, the returns screen has a missing answer. We'd settle that while recording the sale, so the person at the counter doesn't have to invent a refund policy.

Rubix Labs5 min read
Synthetic receipt: three USD 10 items share a USD 10 discount as 3.34, 3.33 and 3.33. Their saved paid amounts are 6.66, 6.67 and 6.67, which return exactly USD 20 in total.
A fictional USD sale and the output of our local arithmetic check. The one-cent remainder belongs to line A under this example's allocation rule. No real payment or customer receipt.

The receipt needs an answer for each item

Our fictional basket has a mug, a notebook and a tote, all USD 10 each. One USD 10 discount applies across the basket. We assume a fully paid sale, one unit per line, and a return policy that refunds each item's original discounted amount. Tax, shipping and fees are outside this example.

Split the discount equally and each item gets USD 3.333... off. Round every discount to USD 3.33 and you've only given USD 9.99 off. Round every paid amount to USD 6.67 and three refunds add up to USD 20.01. We need to place that last cent somewhere.

We'll give it to line A. The mug's saved paid amount is USD 6.66; the other two are USD 6.67 each. Sorting the returns screen differently shouldn't move that cent to another item.

The receipt needs an answer for each item
Receipt lineOriginal priceAllocated discountSaved paid amount
A: mugUSD 10.00USD 3.34USD 6.66
B: notebookUSD 10.00USD 3.33USD 6.67
C: toteUSD 10.00USD 3.33USD 6.67
TotalUSD 30.00USD 10.00USD 20.00

Work in cents, then assign the remainder

For USD, store 10.00 as 1000 integer cents alongside the currency. Stripe's API also expects amounts in the currency's minor unit, but that doesn't mean multiplying every currency by 100: its JPY example uses whole yen. Check the currency and provider rules at the boundary.

Our allocation starts with eligible lines only. For each one, multiply the basket discount by that line's eligible amount, then divide by the eligible subtotal. Keep the whole cents and the remainder separately. With our equal prices, that gives 333 cents per line and leaves one cent unassigned.

Give the leftover cents to the largest fractional remainders first. When remainders tie, use a stable receipt-line ID; A comes before B and C here. Save the resulting allocation with the sale. This is the policy we chose for the example, not a claim about how every POS or payment provider rounds.

Validate the inputs before calculating: integer minor units, positive eligible amounts, unique line IDs and a discount between zero and the eligible subtotal. Use arithmetic that can hold the intermediate products exactly. Python integers handled our local test; a JavaScript implementation would need safe-integer bounds or an appropriate exact numeric type.

Tomorrow's price doesn't change yesterday's mug

Suppose the mug now costs USD 12, or the promotion has ended. The return still starts from its saved receipt allocation. In our example, returning the mug refunds USD 6.66. Returning the tote next refunds USD 6.67, leaving USD 6.67 for the notebook.

Keep the original line price, quantity, discount allocation, currency and paid amount together. Shopify separates the discount's rule from its calculated DiscountAllocation amount. Saving the promotion's name alone would still leave us looking for the amount the customer paid.

A line with several units needs one more decision. If its paid total won't divide evenly, record how the cents belong to individual units, or use a defined cumulative refund rule. Dividing and rounding afresh on every return can spend the same cent twice. Our test uses one unit per line; it doesn't implement either multi-unit policy.

Calculating a refund doesn't send it

The server needs to check the operator's permission and load the saved sale, including earlier returns. We'd reserve a pending refund against the remaining balance in one atomic database operation, so two tills can't both spend the same available amount. That reservation needs to cover the item as well as the payment.

Stripe permits repeated partial refunds up to the remaining unrefunded charge amount. That's a payment-level limit. Your POS still needs its own item-level checks so the same mug doesn't get returned twice while the overall payment has money left.

Keep requested, pending, failed and confirmed refunds distinct, with the provider reference attached. When a response is uncertain, reconcile it before releasing the reservation or trying another refund. Returning an item to stock is a separate decision too: a broken mug can qualify for a refund without going back on the shelf.

What our small test actually checked

We tried all six return orders for this basket with the integer calculation above. Each returned USD 20.00 in total. Changing the input line order kept the same allocation. The in-memory check also rejected a second return of the same item.

We also checked 7,424 small allocation cases, covering line values from one to eight minor units and every discount from zero to the subtotal. Each allocation summed to the requested discount and stayed within its line amount. Seven invalid-input cases were rejected. These are arithmetic checks, not a live payment, tax, database or concurrency test.

For a real build, we'd agree the return policy with the business before extending the calculation. Threshold promotions, tax adjustments, split payments and exchanges can change what needs recording. A rule that claws back a promotion after a return would need its own explicit adjustment; it isn't the refund policy used here.

Try the sales side, then bring us the awkward return

Rubix's Ez-POS work connected sales, inventory, customer records and orders. Our public demo lets you make a discounted sale, inspect its receipt and settle a customer account. It uses fictional data and doesn't process refunds or payments. The refund calculation here is a separate teaching example.

If you're planning a POS with Rubix, bring a sample sale and explain what should happen when part of it comes back. We can help work through the receipt, operator controls and accounting handoff before building the screens. A made-up basket is enough to start that conversation; keep real customer and payment details private.

Rubix Labs

A few finishing touches.