> ## Documentation Index
> Fetch the complete documentation index at: https://waffo.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Tax, Discount & Topup Edge Cases

> The awkward cases: how tax is refunded across several partial refunds so the totals reconcile exactly, when a tax calculation failure stops an order and when the order proceeds untaxed, what each state of a discount record means and which order event drives it, why two discounts cannot be combined on one order, and how long an unpaid Point Topup order holds inventory.

Three subjects meet on this page because they share one property: the normal case
is simple and the edge case is where money goes wrong. Each has a main page that
this one supplements rather than repeats:

* [Fee & Tax Treatment](/docs/en/support/fee-and-tax-treatment-faq) — who bears a tax,
  what the customer is charged in each mode, and how tax appears in your
  settlement files.
* [Coupons & Promotion Codes](/docs/en/support/coupons-and-promotion-codes-faq) — the
  discount engine end to end: defining coupons, issuing codes, redemption records,
  analytics, account-level caps.
* [Point Topup Reseller APIs](/docs/en/support/point-topup-reseller-api-faq) — the six
  reseller endpoints, catalogue and pricing fields, order statuses.

## I refunded an order in stages. How much tax comes back each time?

Each refund gets tax calculated on its own share of the order — **except the last
one**, which is corrected so the totals reconcile exactly.

The reason is rounding. Tax is rounded to two decimals on every calculation, so
across several partial refunds the sum of the individually calculated tax amounts
can drift above or below the tax that was actually collected on the original
payment. Left alone, a customer fully refunded in four instalments could get back
a cent more, or a cent less, than they paid.

So the last refund is not calculated — it is **derived**:

| Refund | Tax refunded |
| - | - |
| Any refund that does not exhaust the order | Calculated on that refund's own amount |
| The refund that exhausts the order | **Original tax collected − tax already refunded** |

"Exhausts the order" means refunds already made plus this one reach or exceed the
original order amount. Failed tax refunds are excluded from the running total, so
a refund that failed on the tax side does not eat into the remainder. And if the
subtraction would come out negative, the amount is floored at zero rather than
being allowed to go the wrong way.

The property this guarantees: **fully refund an order in any number of steps, and
the tax you get back is exactly the tax that was collected.** No accumulated
rounding drift, in either direction.

## What if my refunds add up to more than the original order?

The excess is not refunded. Before any tax is worked out, the refund's taxable
base is clamped against what is left:

| Situation | Taxable base used for this refund |
| - | - |
| Refunds so far plus this one stay within the original amount | This refund's amount, unchanged |
| Refunds so far plus this one exceed the original amount | Original amount **minus** refunds so far |
| Refunds so far already reach or exceed the original amount | **Zero** — no further tax is refunded |

The third row is the one to plan for: once the tax on an order has been fully
refunded, additional refund activity against it produces **no further tax
refund**, silently, without an error. Nothing is over-refunded, but nothing tells
you the tax component was zero either. If you are reconciling tax, reconcile it
against the original payment's tax figure rather than by summing what each refund
appeared to reverse.

## Does a chargeback refund tax the same way as a refund?

It goes through the same cumulative accounting — chargebacks and refunds share
one running total against the original payment, so a chargeback consumes
remaining refundable tax exactly as a refund does, and the last-transaction
correction applies to whichever of them lands last.

One difference in the default: **a chargeback is assumed to be for the full
amount.** Where a partial chargeback occurs, the amount has to be supplied
explicitly; without it the full original amount is what gets reversed.

What a chargeback does to the rest of your money — fees, settlement, the dispute
amount — is a separate subject:
[Fee & Tax Treatment](/docs/en/support/fee-and-tax-treatment-faq).

## Tax could not be calculated on my order. Was the order rejected?

It depends on **why**, and the two outcomes are opposite:

| Kind of failure | Outcome |
| - | - |
| Failures marked as **downgradable** by the tax calculation — including no tax arrangement for the order's country | Tax is **skipped**. The order proceeds at the untaxed amount |
| An unresolvable **postcode / country** combination | The order is **rejected**, with its own distinct reason |
| Anything else — bad currency, missing or invalid calculation inputs | The order is **rejected** as a tax calculation failure |

So "tax calculation failed" is not one behaviour. The downgrade path exists for
the case where tax legitimately does not apply to you for that order, and it is
deliberately silent: a downgraded order looks like an ordinary untaxed order, with
no tax fields populated. It is not an error you will see.

The postcode case is the one worth handling in your integration, because it is
**caused by the data you sent** and it is fixable. An address whose postcode does
not resolve against its country — a transposed code, a country mismatch, a
postcode format the country does not use — fails the order rather than being
downgraded. If you collect addresses yourself, validate the postcode against the
country before you create the order; if the checkout page collects them, the
customer is prompted to correct it.

There is also a short retry on a timeout, so a single slow calculation does not
by itself fail your order.

## Why would tax not be calculated on my order at all?

Because tax arrangements are held **per country**, not per account. Having one
does not give you all of them.

For an order in a country you have no arrangement for, there is nothing to
calculate against, so the calculation reports a downgradable failure and the order
proceeds untaxed. That is the normal, intended path — not a misconfiguration.

Two further properties of the per-country model that explain otherwise puzzling
behaviour:

* **The inputs needed to *calculate* a tax and the inputs needed to *remit* it are
  two different sets**, defined separately per country. An order can therefore
  carry everything needed to compute tax and still be incomplete for remittance
  purposes, or the reverse. Which fields a given country requires comes from your
  arrangement, not from a universal list.
* **The country that determines the tax is not necessarily where it is paid.**
  Both are recorded per arrangement, and they can differ.

Who is charged the tax and who bears it — and why a merchant-borne tax is not
shown to your customer at checkout — is covered in
[Fee & Tax Treatment](/docs/en/support/fee-and-tax-treatment-faq).

## Why is my redemption record still `CREATED`?

Because the order it belongs to has not reached a final state yet. A discount
record is **reserved at order creation and only confirmed later**, which means
`CREATED` is normal for an order still in progress, not a stuck record.

The record advances when the order does:

| The order… | Record becomes | Effect on the redemption slot |
| - | - | - |
| Is still in progress | `CREATED` | Held — reserved, not consumed |
| Reached a successful payment | `COMPLETED` | Consumed |
| Closed without a successful payment | `VOIDED` | Released back to the coupon and to the code |
| Failed during creation, before it was even recorded | `VOIDED` | Released |
| Was refunded after being confirmed | `REVERSED` | Released, **subject to the refund being full** |

Consequences worth designing around:

* **`CREATED` reservations hold stock.** An abandoned checkout holds its slot until
  the order closes. If a limited coupon looks exhausted while your redemption
  history shows fewer confirmed uses than the limit, unclosed reservations are
  usually the difference.
* **Only `COMPLETED` counts.** Analytics counts confirmed redemptions, so a record
  in any other state will not appear in your discount reporting.
* **A record can be voided without your order ever existing.** Where order creation
  fails after a discount was reserved but before the order was persisted, the
  reservation is still released — you will see a `VOIDED` record with no order
  behind it. That is cleanup, not a phantom order.

## Does a refund give the redemption slot back?

Only a **full** refund does, and only under the account's reversal strategy. A
partial refund leaves the record `COMPLETED` and the slot consumed.

The reasoning is that a partial refund has not undone the purchase, so the
discount was still used. The practical consequence is that a customer who buys
with a `ONCE` coupon and takes a partial refund does not get the coupon back.

The strategy setting, what a zero-amount subscription trial does to the slot, and
why reversal can never push a counter negative are all covered in
[Coupons & Promotion Codes](/docs/en/support/coupons-and-promotion-codes-faq).

## Can a customer combine two discounts on one order?

No. **One order carries one discount.** There is no stacking, in either direction:
you cannot apply a second discount to an order that already has one, and a
customer cannot add one to an order you already discounted.

Three separate rules enforce it, with three distinct messages:

| What you did | Message |
| - | - |
| Sent both a promotion code and a coupon ID on the same order | `promotionCode and couponId are mutually exclusive` |
| Tried to apply a discount to an order that already has a different one | `order already has a different promotion applied` |
| Sent a promotion code that does not belong to the coupon you named | The code is refused as not found or invalid |

All three come back under `A0003`, with the specific message carried in the
response — see
[Reading Your Payment Method Configuration](/docs/en/support/payment-method-config-and-limits-faq)
for how `A0003` details are formatted.

If you need "two offers at once" commercially, that is one coupon configured for
the combined value, not two coupons on one order.

## What does "discount calculation result has expired" mean?

The discount amount you were quoted is **time-limited**, and you tried to create
the order with a stale one.

Calculating a discount and creating the order are two steps: the calculation
returns what the customer will pay, and the order create references that result.
If too long passes between them — a customer who left the page open, a checkout
flow that holds the quote across a session — the referenced result is no longer
accepted and the order is refused, with a message telling you to recalculate.

Recalculating is the fix, and it should be automatic: on that error, re-run the
discount calculation and create the order against the fresh result. Do not treat
it as a coupon problem and do not show the customer a discount failure — the
coupon is fine, only your quote went stale. Be prepared for the recalculated
amount to differ, since eligibility is re-checked: a code that hit its redemption
limit in the meantime will now be refused outright.

## Which discount errors mean my setup is the problem, not the customer's?

These come back on order creation, all under `A0003`. The split matters because
half of them are yours to fix and half are simply the offer working as
configured:

| Message | Whose problem |
| - | - |
| `coupon feature is not enabled for this merchant` | **Yours** — the account-level master switch is off. No coupon will ever apply until it is on |
| `promotionCode and couponId are mutually exclusive` | **Yours** — an integration bug |
| `order already has a different promotion applied` | **Yours** — the request is trying to stack |
| `discount calculation result has expired, please recalculate` | **Yours** — recalculate and retry |
| `promotion code not found or invalid` | Either — a typo, or a code that was never issued |
| `promotion code has expired` | The offer, working as configured |
| `coupon has expired or been archived` | The offer, working as configured |
| `promotion code redemption limit has been reached` | The offer, working as configured |
| `order amount does not meet the minimum requirement for this promotion` | The customer's basket is too small |

The first row is the one that wastes the most time, because it looks like a coupon
problem: every coupon you create will appear valid in the Portal and be refused at
order creation. If **nothing** discounts, check the master switch before checking
the coupon.

The full eligibility order, including per-customer and first-time-only rules that
depend on you sending a customer identifier, is in
[Coupons & Promotion Codes](/docs/en/support/coupons-and-promotion-codes-faq).

## How long does an unpaid Point Topup order hold inventory?

Not indefinitely. Creating a Point Topup order **reserves** the stock it needs,
and if the order is never paid, that reservation is released automatically after
a default holding period (an operational setting, not a contract term — do not
build a countdown on it).

Two consequences:

* **An unpaid order is not a hold you can rely on.** If you are staging orders
  ahead of payment, or letting a customer come back later to pay, the item may no
  longer be available when they do. Where availability matters, pay promptly rather
  than creating the order early.
* **Released stock does not resurrect the order.** The order remains in its
  original state; releasing the reservation frees the item for other buyers, it
  does not cancel or reprice anything on your side.

Where an order fails outright, any amount already debited is returned to your
balance — see
[Point Topup Reseller APIs](/docs/en/support/point-topup-reseller-api-faq).

## Can one Point Topup order carry more than one item?

No. A Point Topup order carries exactly **one SKU, with a quantity of one**. Any
other quantity is rejected at creation.

To buy several items, create several orders. Each gets its own idempotency key,
its own reservation and its own outcome, which is generally what you want anyway:
one item failing does not take the others with it.

Note also that a status of `PAY_SUCCESS` on such an order means the payment
succeeded and **fulfillment is yours to complete** — Waffo does not deliver that
SKU. Do not read `PAY_SUCCESS` as "the customer has their goods". The field
reference and the other statuses are on
[Point Topup Reseller APIs](/docs/en/support/point-topup-reseller-api-faq).

## Where do I look next?

| Question | Page |
| - | - |
| Who bears a tax, and how tax appears in settlement | [Fee & Tax Treatment](/docs/en/support/fee-and-tax-treatment-faq) |
| Defining coupons, issuing codes, redemption reporting | [Coupons & Promotion Codes](/docs/en/support/coupons-and-promotion-codes-faq) |
| The reseller endpoints, catalogue, statuses and notifications | [Point Topup Reseller APIs](/docs/en/support/point-topup-reseller-api-faq) |
| Partial refunds, refund preconditions, cross-currency refund amounts | [Refund FAQ](/docs/en/support/refund-faq) |
| How `A0003` reports which field was wrong | [Reading Your Payment Method Configuration](/docs/en/support/payment-method-config-and-limits-faq) |
| Which address fields a payment method requires | [Conditional & Required Fields](/docs/en/support/conditional-required-fields-faq) |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.