# Upsells

Manual per-product upsell associations, surfaced in a **"Complete your basket"**
overlay after a user adds an assessment-recommended product. Lets admins
configure add-on products that are offered inline before the customer continues
to the basket.

Cloned from `ComplimentedItems`, but manual-config-only (no auto-supplement or
member-specific logic) and built as a modern Laravel plugin (ServiceProvider +
config + migration).

## How to install

Register the service provider in `/config/app.php` (already added in this repo):

```php
\Mtc\Plugins\Upsells\Providers\UpsellsServiceProvider::class,
```

Run migrations to create the `items_upsells` table:

```sh
php artisan migrate
```

The provider auto-loads the migration and registers the admin tab listener — no
other wiring is required.

## Configuration

`config/upsells.php` (merged under the `upsells` key):

| Key | Env | Default | Purpose |
|-----|-----|---------|---------|
| `enabled` | `UPSELLS_ENABLED` | `true` | Master enable/disable for the whole feature |
| `max_rows` | `UPSELLS_MAX_ROWS` | `8` | Max upsell products configurable per product in the admin |

When `enabled` is false the admin tab is hidden, `getUpsellsFor()` returns an
empty array, and `decorateWithUpsells()` is a no-op.

## Admin usage

A per-product **Upsells** tab is added to the product admin tabnav (via the
`Mtc\Shop\Events\ItemAdminTabnavEvent` listener), linking to:

```
/plugins/Upsells/admin/?id={item_id}
```

The page offers up to `max_rows` product-picker rows (Browse / Clear), using the
shared product selector (`manage.products.mini.php`). Save replaces the full set
for that product atomically (delete + re-insert wrapped in a DB transaction).

**Eligibility on save:** assessment-required products are rejected (they can
never be upsells) and reported back. Stored upsells that have *since* become
assessment-required are flagged in the UI but kept until reconfigured.

## How it works

### Eligibility (single source of truth)

`Item::requiresAssessment()` (`shop/src/Item.php`) mirrors the gate in
`shop/addtobasket.php`:

```
product_type ∈ ['doctor', 'pharmacy']
  AND NOT disable_assessment_forms
  AND (item.form_id > 0 OR main_category.form_id > 0)
```

Only products that are **not** assessment-required may be upsells. This predicate
is used by both the admin save step and the live frontend filter.

### Backend

`ShopHelper::decorateWithUpsells()` attaches a shaped `upsells` array to each
assessment product in `ShopController::get_basket_allowed_products()`. Upsells
are resolved live through `Upsells::getUpsellsFor()`, which drops products that
are assessment-required, hidden, deleted, or out of stock — so a configured
upsell that later becomes ineligible disappears automatically with no admin
change.

### Frontend

The overlay component is mounted once in the assessment layout
(`sites/default/templates/layouts/assessment.twig`):

```twig
{% if disable_vue is empty %}
    <upsells-popup></upsells-popup>
{% endif %}
```

It is opened from `add_to_basket.vue` on a successful add when the product
carries upsells, via the global event bus:

```js
window.EventBus.$emit('upsells:open', {
    addedProduct: vm.product,
    upsells: vm.product.upsells,
    proceedUrl: '/shop/checkout/basket.php',
});
```

The overlay pins the just-added product, lists eligible upsells (hiding any
already in the basket), supports inline **+ Add**, and proceeds to `proceedUrl`
on Skip / Continue. Products **without** upsells keep the existing inline
success-message behaviour.

## Data model

Table `items_upsells` — one row per (product → upsell product) association:

| Column | Type | Notes |
|--------|------|-------|
| `id` | increments / PK | |
| `item_id` | integer, indexed | The product being viewed (owner) |
| `upsell_item_id` | integer, indexed | The product offered as an upsell |
| `order` | smallInteger | Display order in the overlay |
| `created_at` / `updated_at` | timestamps | |

## Files

| Path | Purpose |
|------|---------|
| `Classes/Upsells.php` | Eloquent model, `addTabNav()`, `assessmentRequiredIds()`, `getUpsellsFor()` |
| `Providers/UpsellsServiceProvider.php` | Config merge, migration load, admin tab listener |
| `config/upsells.php` | `enabled` / `max_rows` settings |
| `database/migrations/*_create_items_upsells_table.php` | `items_upsells` schema |
| `admin/index.php` | Per-product management page |
| `admin/js/script.js` | Vanilla ES6 picker (Browse / Clear) |
| `templates/Upsells/admin/tabnav.twig` | "Upsells" admin tab `<li>` |

Frontend assets live in the site theme:

- `sites/default/js/components/shop/upsells/upsells_popup.vue`
- `sites/default/js/components/shop/upsells/upsells_row.vue`
- `sites/default/css/elements/upsells_popup.less`
