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

# Refunds & Cancellations

> Rules, windows, and partial refund behaviour

This page explains how Points handles order reversals after creation.

There are two distinct operations:

| Operation | Endpoint                        | Typical use                                       |
| --------- | ------------------------------- | ------------------------------------------------- |
| Cancel    | `POST /v1/orders/{uuid}/cancel` | The order should no longer continue               |
| Refund    | `POST /v1/orders/{uuid}/refund` | Money/points need to be returned after settlement |

## Cancel an order

Use cancellation when the order should be stopped rather than financially reversed later.

Example:

```bash theme={null}
curl -X POST https://business.papp.sa/api/v1/orders/550e8400-e29b-41d4-a716-446655440000/cancel \
  -H "x-api-key: $POINTS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Customer requested cancellation" }'
```

### Current behavior

* validates merchant ownership of the order
* rejects already-cancelled or already-refunded orders
* defaults the reason to `"Order cancelled via API"` if omitted
* emits webhook event `cancelled`

## Refund an order

Use refund after an order is already settled and value must be returned.

Example full refund:

```bash theme={null}
curl -X POST https://business.papp.sa/api/v1/orders/550e8400-e29b-41d4-a716-446655440000/refund \
  -H "x-api-key: $POINTS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

Example partial refund:

```bash theme={null}
curl -X POST https://business.papp.sa/api/v1/orders/550e8400-e29b-41d4-a716-446655440000/refund \
  -H "x-api-key: $POINTS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 25.00,
    "reason": "One line item returned"
  }'
```

### Current behavior

* supports both full and partial refunds
* emits webhook event `refunded`
* updates `order_status` to `fully_refunded` or `partially_refunded`

## How Points value is reversed

### Replacing (redeem) orders

For a redeem checkout, refunding reverses the redeemed value and updates the order status accordingly.

### Earning orders

For an earning order, refunding reverses previously awarded points. In partial refunds, the backend calculates the proportional points to reverse and includes remaining amounts in the refund payload used internally.

## Auto-refund edge case

There is also an internal system event called `auto_refunded`. This can occur when the backend automatically reverses a replacing order because an expected transaction record was missing during reconciliation. Integrators should treat it as a refund-class terminal state and handle it in webhook consumers.

## Merchant-dashboard refunds (storefront orders)

Merchants who sell their own physical / digital products **through the Points storefront** (not pure API integrators) can refund a `ProductOrder` from the dashboard. The button is on the Order details page (`Product Orders → show`) and is more than a gateway refund — it runs a **full-stack reversal** in a fixed order:

| Step | What happens                                                                                                                                      | Reversible?                                                         |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| 1    | Gateway refund — Tabby, Tamara, or Moyasar (VISA / MADA / Apple Pay / MasterCard) is refunded for the captured amount                             | **No** — once accepted by the gateway, money is back with the buyer |
| 2    | Linked `EARNING` order is fully refunded — points awarded to the buyer are withdrawn back, merchant's wallet is credited back                     | Yes (admin can re-issue)                                            |
| 3    | Linked `REPLACING` order is fully refunded — points the buyer redeemed are returned to their wallet, merchant's `settle` wallet entry is reversed | Yes                                                                 |
| 4    | `metadata.refund_status = 'refunded'` is stamped on the ProductOrder, the badge flips to **"Refunded"**, and the trigger button disappears        | Yes                                                                 |

If step 1 fails (gateway rejects, captured amount mismatch, missing reference), the whole flow aborts — nothing else is touched. If steps 2-3 fail after step 1 succeeded, the metadata is still stamped (the money is already back with the buyer) and the failures land in the server log for manual reconciliation.

### 3-day refund window

The dashboard refund button **disappears 3 days after the order was created**. After that, the weekly settlement run (`MakeProductSettlements`, Thursday in Riyadh time) has already swept the order into the merchant's payout, so an automated reversal would race the settlement and leave the books out of sync.

| Order age   | Refund button | Notes                                                                                   |
| ----------- | ------------- | --------------------------------------------------------------------------------------- |
| \< 72 hours | **Visible**   | Standard flow above; runs end-to-end via `RefundProductOrderService`                    |
| ≥ 72 hours  | Hidden        | Settlement has captured the order; refund needs to be coordinated with finance manually |

Direct calls to the service (e.g. from admin tooling or a stale browser tab) are guarded by the same window — past 72 hours they return:

```json theme={null}
{ "ok": false, "message": "The amount cannot be refunded after 3 days from the transaction date." }
```

### Settlement skips refunded orders

Once `metadata.refund_status = 'refunded'` is stamped, the weekly product-settlement job excludes the order from the merchant's next payout — so the merchant is never paid for an order whose money already went back to the buyer.

<Note>
  This whole section is for the **merchant dashboard** flow. The public `POST /v1/orders/{uuid}/refund` endpoint (described above) is the API equivalent for loyalty `Order` reversals and is **not** subject to the 3-day window.
</Note>

## Recommended merchant policy

<AccordionGroup>
  <Accordion title="Use cancel before fulfilment">
    If the order should simply stop and has not entered your final fulfilment path, cancel is usually cleaner than refund.
  </Accordion>

  <Accordion title="Use refund after settlement">
    If value was already settled, use the refund endpoint so Points can properly reverse the financial and loyalty effects.
  </Accordion>

  <Accordion title="Persist refund reasons">
    Even when not required, always send a human-readable `reason` so support teams can reconcile merchant actions against webhook history.
  </Accordion>
</AccordionGroup>

## Webhooks you should expect

| Action                      | Webhook         |
| --------------------------- | --------------- |
| Cancel                      | `cancelled`     |
| Refund                      | `refunded`      |
| Internal automatic reversal | `auto_refunded` |

## Next

<CardGroup cols={2}>
  <Card title="Order Lifecycle" icon="timeline" href="/integration/order-lifecycle">
    See where cancellation and refund fit in the state machine.
  </Card>

  <Card title="Webhook Events" icon="list" href="/webhooks/events">
    Payload examples for `cancelled`, `refunded`, and related events.
  </Card>
</CardGroup>
