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

# Discounts

> Preset and manual discounts, comps, role-based access, and max discount enforcement.

The discount system supports item-level and check-level discounts with configurable presets, manual entry, comps (100% discount), role-based access control, and manager override for restricted users.

## Discount types

Presets are configured in the Odoo backend (`nu.pos.discount.preset`):

| Type            | Behavior                                                               |
| --------------- | ---------------------------------------------------------------------- |
| `fixed_percent` | Fixed percentage (e.g., "10% Off") — value pre-filled, not editable    |
| `fixed_amount`  | Fixed dollar amount (e.g., "\$5 Off") — value pre-filled, not editable |
| `open_percent`  | Opens percentage entry with the preset as default — user can change    |
| `open_amount`   | Opens amount entry with the preset as default — user can change        |

Each preset has an `applies_to` field: `item`, `check`, or `both`.

### Comp

A comp is a 100% discount applied directly (no modal). Triggered from the "Comp Item" tile in the item detail panel. If the user's `max_discount` is below 100, a manager override is required.

## Access control

<Steps>
  <Step title="Permission check">
    User clicks discount/comp. System checks `canPerformAction(role, "discount", permissions)`.
  </Step>

  <Step title="Allowed">
    Proceed to `DiscountModal` (or apply comp directly for 100% discount).
  </Step>

  <Step title="Denied">
    Open `ManagerOverrideModal` for PIN approval by an authorized manager.
  </Step>
</Steps>

### Max discount enforcement

Each user has a `max_discount` value from their `PosUser` record:

* `0` = unlimited (no cap)
* `> 0` = maximum percentage this user can apply

The `DiscountModal` clamps input to `maxDiscount`. When a manager approves via override, their own `max_discount` is used. In the override modal, managers with insufficient `max_discount` are shown grayed out.

## Data flow

### Item discount

```
Tap "Discount Item"
  → Permission check (or manager override)
  → DiscountModal (context: "item")
  → User selects preset or enters value
  → APPLY_ITEM_DISCOUNT { itemId, discount (%), discountState }
  → item.discount and item.discountState updated
```

### Check discount

```
"More" → "Discount"
  → Permission check (or manager override)
  → DiscountModal (context: "check")
  → User selects preset or enters value
  → distributeCheckDiscount() — proportional across non-voided items
  → APPLY_CHECK_DISCOUNT { checkDiscount, itemDiscounts (Map) }
```

### Stacking

Item and check discounts compose multiplicatively: 10% item + 20% check = 28% total, not 30%.

## Reducer actions

| Action                  | Payload                               | Effect                                            |
| ----------------------- | ------------------------------------- | ------------------------------------------------- |
| `APPLY_ITEM_DISCOUNT`   | `{ itemId, discount, discountState }` | Sets `item.discount` (%) and `item.discountState` |
| `REMOVE_ITEM_DISCOUNT`  | `{ itemId }`                          | Clears discount to 0                              |
| `APPLY_CHECK_DISCOUNT`  | `{ checkDiscount, itemDiscounts }`    | Sets order.checkDiscount and per-item discount %  |
| `REMOVE_CHECK_DISCOUNT` | —                                     | Clears all discounts                              |

## Audit trail

Both `ItemDiscountState` and `CheckDiscountState` store:

* `appliedByUserId` / `appliedByName` — who applied the discount
* `approvedByUserId` / `approvedByName` — manager who approved (if override was needed)
* `presetId` / `presetName` — which preset was used (if any)

## Hub sync

`discountState` and `checkDiscount` flow through the snapshot pipeline:

* `buildSnapshot()` includes them on items and orders
* `rebuildOrderFromSnapshot()` restores them

## Backend addon

`nu_pos_discounts/` — Odoo 12 addon. Extends `_get_bootstrap_extra()` to include `discount_presets` array:

```json theme={null}
{
  "discount_presets": [
    { "id": 1, "name": "10% Off", "discount_type": "fixed_percent", "value": 10, "applies_to": "both" },
    { "id": 2, "name": "$5 Off", "discount_type": "fixed_amount", "value": 5, "applies_to": "item" }
  ]
}
```

## i18n

Translations in the `discounts` namespace (`src/i18n/locales/{en,es}.json`).

| Key                   | EN                         | ES                         |
| --------------------- | -------------------------- | -------------------------- |
| `discountItem`        | Discount Item              | Descuento a Ítem           |
| `discountCheck`       | Discount Check             | Descuento a Cuenta         |
| `removeDiscount`      | Remove Discount            | Quitar Descuento           |
| `maxDiscountExceeded` | Maximum discount is {max}% | Descuento máximo es {max}% |

## Testing

```bash theme={null}
cd nu_pos_react
npm test -- src/domain/discounts/
```

* `models.test.ts` — Parsing, preset filtering
* `services.test.ts` — `computeDiscountPct`, `distributeCheckDiscount`, `composeDiscountPct`
