On a cash sale with change due, the transactioncomplete event reports
balanceDue.amount === 0 and a paymentMethods entry equal to the order total,
even though POS itself displays the change on its own sale-complete screen. The
amount tendered and the change due appear nowhere in the payload. This
contradicts two explicit statements in the field documentation.
Environment
@shopify/ui-extensions 2026.7.0,api_version = "2026-07"- Target:
pos.app.ready.data, subscribed via
shopify.addEventListener('transactioncomplete', …) - POS iOS app -
11.13.0
What the documentation says
From the app-background target reference,
SaleCompleteEvent:
paymentMethods(Payment[]) — An array of all payment methods used to
complete this transaction. Each payment entry specifies the payment type (for
example, cash, credit card), amount tendered, and currency. Multiple
entries indicate split payments where the customer paid using multiple methods
(for example, part cash, part credit card). The sum of all payment amounts
should equal or exceed thegrandTotal.
balanceDue(Money) — The remaining balance still owed on this
transaction as aMoneyobject. Typically zero for fully paid transactions. A
positive balance indicates partial payment or layaway scenarios. A negative
balance indicates overpayment, where change should be returned to the
customer. Calculated as: grandTotal minus sum of all payment amounts.
What actually happens
Test A — exact payment (control). Order total ¥33, customer paid ¥33.
grandTotal: { amount: 33, currency: 'JPY' }
paymentMethods: [ { amount: 33, currency: 'JPY', type: 'Cash' } ]
balanceDue: { amount: 0, currency: 'JPY' }
Consistent with the docs — but it cannot distinguish the two readings, because
tendered and applied are the same number. This is why the problem is easy to
miss.
Test B — over-tender. Order total ¥15, customer handed over ¥100. POS
displayed (Change due: ¥85) on its own sale-complete screen.
grandTotal: { amount: 15, currency: 'JPY' }
paymentMethods: [ { amount: 15, currency: 'JPY', type: 'Cash' } ]
balanceDue: { amount: 0, currency: 'JPY' }
tipAmount: undefined
cashRoundingAdjustment: undefined
Neither 100 (tendered) nor 85 (change) appears anywhere in the payload —
I logged the whole event object, not just these fields.
Both fields are present and populated. They simply carry the amount applied to
the order rather than the amount tendered, which is the same semantics as
the Admin API’s OrderTransaction.amountSet. Under that reading:
paymentMethods[].amountcan only ever equalgrandTotal, never exceed it,
so the documented “equal or exceed” case is unreachable.balanceDueis then always exactly 0 for a completed sale, so the documented
“negative balance indicates overpayment” case is also unreachable..
Minimal repro
export default async function background() {
shopify.addEventListener('transactioncomplete', (event) => {
console.log(JSON.stringify(event, null, 2));
});
}
api_version = "2026-07"
[[extensions.targeting]]
module = "./src/Background.ts"
target = "pos.app.ready.data"
Then ring up any item and pay cash with more than the total. Reproducible on
every over-tendered cash sale.
Questions
- Is the applied-amount behaviour intended, making the “amount tendered” and
“negativebalanceDue” wording inaccurate? Or are the values wrong? - If it is a defect, is there a tracking issue I can follow?
- Is there any supported way for an extension to read the cash amount
tendered and the change due for a completed sale? I have already ruled out
the Admin API:Orderexposes no tendered/change field,
OrderTransaction.amountSetis the applied amount, andOrder.receiptJson
came back empty for POS orders in my testing. The ¥85 also does not appear on
the emailed or SMS receipt — only on the POS completion screen — which
suggests POS computes it for display without persisting it anywhere. - Are
tipAmountandcashRoundingAdjustmentever populated for JPY cash
sales, or only under specific configuration?