> For the complete documentation index, see [llms.txt](https://docs.wynta.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.wynta.com/bonus/architecture/lifecycle-and-chunks.md).

# Bonus Lifecycle & Chunk Model

## State machine

The lifecycle applies **at the chunk level**. A bonus is the aggregate of its chunks; the grant carries a derived status (e.g. `PARTIALLY_RELEASED`) summarizing them.

```
Release trigger event fires (deposit, registration, …)
    │  code validated earlier via POST /validate-code (read-only)
    ▼
GRANT created — split into no_of_chunks chunks, all PENDING ──────┐
    │  release trigger fires and/or wagering                       │ chunk_expiry_days /
    ▼  progress meets the chunk requirement                        │ bonus_expiry_days reached
RELEASE (per chunk — spendable)  ─────────────┐                    ▼
    │  consumed against wagers                │                EXPIRED (terminal)
    ▼                                         ▼
CONSUMED (terminal)                  EXPIRED / FORFEITED (terminal)
```

Chunk `status` values (transaction-detail API): **`PENDING → RELEASE → CONSUMED / EXPIRED`**. A chunk must be in `RELEASE` status before it can be consumed. Ledger event `type` values: `grant`, `released`, `consumed`, `expiry`, `forfeited`.

### State-to-wallet mapping (PAM client wallet)

| Chunk transition       | Ledger event | PAM Bonus Wallet effect                                     |
| ---------------------- | ------------ | ----------------------------------------------------------- |
| grant → `PENDING`      | `grant`      | Increment **Pending**                                       |
| `PENDING` → `RELEASE`  | `released`   | Move chunk amount **Pending → Available**                   |
| `RELEASE` → `CONSUMED` | `consumed`   | Decrement **Available** (PAM debits first; ledger confirms) |
| → `EXPIRED`            | `expiry`     | Remove from the relevant bucket                             |
| → forfeited            | `forfeited`  | Remove remaining from Pending and/or Available              |

Wynta's authoritative per-chip-type view (`GET /{user_id}/summary`): `bonus_balance` (available = released − consumed), `pending_bonus`, and `wagering_required` (remaining wagering to release all pending chunks).

## Chunk configuration

Chunking is configured per bonus on the **configure node** (`bonus_create_configure`) and snapshotted onto each grant (visible in transaction detail):

| Field               | Default | Meaning                                                                              |
| ------------------- | ------- | ------------------------------------------------------------------------------------ |
| `no_of_chunks`      | `1`     | Number of equal chunks the granted bonus is split into                               |
| `wager_multiplier`  | `0`     | Wagering requirement multiplier; `0` = no wagering — chunks release on trigger alone |
| `wager_chip_type`   | `CASH`  | Chip type whose wagers count toward release progress (may be `BONUS`)                |
| `credit_chip_type`  | `CASH`  | Chip type the bonus is credited in                                                   |
| `chunk_expiry_days` | —       | Days before an unreleased chunk expires                                              |
| `bonus_expiry_days` | —       | Days before the entire bonus expires                                                 |
| `release_bucket`    | —       | Optional release bucket identifier                                                   |

## Chunk attributes (transaction detail API)

`GET /api/v1/user-bonuses/{user_id}/transactions/{txn_id}` returns each grant with its chunks:

| Field                       | Description                                                   |
| --------------------------- | ------------------------------------------------------------- |
| `id` / `chunk_ref`          | Chunk record ID / human-readable reference (e.g. `CHUNK_001`) |
| `chunk_amount`              | Bonus amount in this chunk = `grant_amount / no_of_chunks`    |
| `wager_multiplier`          | Wagering multiplier for this chunk                            |
| `status`                    | `PENDING` \| `RELEASE` \| `CONSUMED` \| `EXPIRED`             |
| `wager_amount`              | **Wagering progress toward release, tracked per chunk**       |
| `created_at` / `updated_at` | Creation / last status-change timestamps                      |

Grant-level rollups: `grant_amount`, `release_amount`, `bonus_consumed`, derived `status`.

## How a chunk releases

Two mechanisms move a chunk `PENDING → RELEASE`, per the bonus configuration:

1. **Release trigger events** — configured rules (`trigger_type` = `DEPOSIT`, `LOGIN`, `BET_PLACED`, `TOURNAMENT_WON`, …) that fire when a matching [PAM event](/bonus/api-reference/bonus-events.md) arrives and its conditions pass (`occurrence`, amount band, `payment_method`, `product`, `trigger_config_json`).
2. **Wagering progress** — when `wager_multiplier > 0`, each qualifying `bet_placed` event adds its `wager_amount` to the current chunk's progress; the chunk releases when progress meets the chunk requirement:

```
chunk_requirement = chunk_amount × wager_multiplier
on bet_placed event (matching wager_chip_type):
    chunk.wager_amount += event.wager_amount
    if chunk.wager_amount >= chunk_requirement:
        chunk.status = RELEASE            # PENDING → RELEASE
        emit ledger event "released" + webhook BONUS_RELEASED
        (overflow carries into the next PENDING chunk)
```

{% hint style="success" %}
**Example:** ₹500 bonus, `wager_multiplier = 3.00`, `no_of_chunks = 3` → three chunks of ₹166.67, each releasing after ₹500.01 of qualifying wagering (₹1,500 total). `GET /{user_id}/summary` exposes the remaining total as `wagering_required`.
{% endhint %}

Rules:

* **Serialization** — progress updates are serialized per player-bonus; concurrent `bet_placed` events cannot double-release a chunk.
* **Overshoot** — a large wager can complete several chunks at once; the webhook may batch them in a `chunks[]` array.
* **Monotonic release** — voided wagers reduce progress but already-released chunks are never clawed back.
* **`wager_multiplier = 0`** — no wagering requirement; chunks release on their trigger conditions alone.
* **Multiple active bonuses** — if a player holds several active wagering bonuses, contribution is attributed to the **oldest active bonus first**; the recommended policy is to enforce a **single active wagering bonus per chip type** to avoid attribution disputes (open question — see [Operations](/bonus/operations/nfr-and-failures.md#open-questions)).

## Worked example

₹500 grant, 3 × ₹166.67 chunks, 3× multiplier (₹500 wagering per chunk):

| Cumulative qualifying wagered | Chunks in `RELEASE` | `pending_bonus` | `bonus_balance` | `wagering_required` |
| ----------------------------- | ------------------- | --------------- | --------------- | ------------------- |
| ₹0                            | 0                   | 500.00          | 0.00            | 1,500.00            |
| ₹500                          | 1                   | 333.33          | 166.67          | 1,000.00            |
| ₹1,100                        | 2                   | 166.67          | 333.33          | 400.00              |
| ₹1,500                        | 3                   | 0.00            | 500.00          | 0.00                |

## Consumption across chunks

The consume endpoint **auto-selects the user's next available released chunk** — the client never chooses chunks. Every consumption is recorded per chunk under its `consume_txn_id`, so transaction detail shows exactly which chunks funded which rounds — this is what makes reversal and reconciliation exact.

## Expiry & forfeit at chunk level

Two independent clocks:

* **Chunk expiry** (`chunk_expiry_days`) — an unreleased chunk expires on its own; recorded as an expiry event with `type: "CHUNK_EXPIRY"`.
* **Bonus expiry** (`bonus_expiry_days`) — the whole bonus expires; non-terminal chunks transition together, recorded with `type: "BONUS_EXPIRY"`.

Forfeits carry a `type` (`MANUAL`, `RULE_VIOLATION`) and the triggering `operator`. On expiry or forfeit the webhook carries per-chunk detail so the PAM decrements Pending and Available by the correct split.
