# Product Compositions — Implementation Notes

## What is a Composition?

A **composition** (analogous to a Linnworks "Composite Item") is a product that is assembled from two or more component SKUs. The parent SKU represents the finished/assembled item; the component SKUs are its constituent parts.

**Example:** "Gift Set A" (parent SKU: GIFT-A) = 1× Mug (MUG-01) + 1× Coaster (COAST-01) + 2× Biscuit packs (BISC-05)

---

## Database

### New table: `products_product_compositions`

| Column               | Type          | Notes                                      |
|----------------------|---------------|--------------------------------------------|
| id                   | bigint        | PK                                         |
| parent_product_id    | bigint FK     | → products_products (the composed product) |
| component_product_id | bigint FK     | → products_products (the ingredient)       |
| quantity             | decimal(15,4) | How many components per 1 unit of parent   |
| sort                 | integer       | Display / reorder position                 |
| company_id           | bigint FK     | → support_companies                        |
| creator_id           | bigint FK     | → users                                    |
| timestamps           |               |                                            |

### New column: `products_products.is_composition`

| Column         | Type    | Default | Notes                                           |
|----------------|---------|---------|-------------------------------------------------|
| is_composition | boolean | false   | Set automatically when components are added/removed |

---

## Models

### `Webkul\Product\Models\ProductComposition`
- File: `plugins/webkul/products/src/Models/ProductComposition.php`
- Table: `products_product_compositions`
- Relations:
  - `parentProduct()` → BelongsTo Product (via `parent_product_id`)
  - `componentProduct()` → BelongsTo Product (via `component_product_id`)
  - `company()`, `creator()`

### `Webkul\Product\Models\Product` — additions
- `is_composition` added to `$fillable` and `$casts`
- `compositions()` → HasMany ProductComposition (via `parent_product_id`), ordered by `sort`
- `usedInCompositions()` → HasMany ProductComposition (via `component_product_id`)

---

## Inventory Product Tab: Compositions

### File
`plugins/webkul/inventories/src/Filament/Clusters/Products/Resources/ProductResource/Pages/ManageCompositions.php`

### Route
`/inventory/products/{record}/compositions`

### Registered in
`plugins/webkul/inventories/src/Filament/Clusters/Products/Resources/ProductResource.php`
- Added to `getRecordSubNavigation()` between Variants and Quantities
- Added to `getPages()` as `'compositions' => ManageCompositions::route('/{record}/compositions')`

### Behaviour
- Lists components with SKU, Name, Qty, Unit Cost columns
- Rows are drag-reorderable (`sort` column)
- **Add Component**: opens modal; select component product + quantity
  - On create: sets `is_composition = true` on the parent automatically
- **Delete**: on delete, if no more components remain, sets `is_composition = false` automatically
- Component product list excludes: configurable products, the parent itself, non-GOODS products

---

## Purchase Order Integration

### Problem
When a composition product is added to a Purchase Order, some suppliers supply the assembled parent SKU, while others need individual component SKUs on the PO. The system must support both without forcing one behaviour.

### Solution
When a composition product is selected in the PO order lines repeater:

1. `afterProductUpdated()` in `OrderResource` detects `$product->is_composition = true` and sets the `_is_composition` flag in the repeater row state.
2. A **"Composition: Order as"** select field becomes visible for that row, offering:
   - **Parent SKU (1 line)** — default; the PO line is the assembled product exactly as entered.
   - **Expand to components (individual child SKUs)** — on save, that row is replaced with one line per component.
3. The expansion happens in `mutateFormDataBeforeSave` (EditOrder) and `mutateFormDataBeforeCreate` (CreateOrder) by calling `EditOrder::expandCompositionLines()`.

### Key files modified

| File | Change |
|------|--------|
| `OrderResource.php` | `afterProductUpdated()` sets `_is_composition`; repeater schema adds `_is_composition` (hidden) and `_composition_mode` (select, conditional); table column `_composition_mode` added |
| `EditOrder.php` | `mutateFormDataBeforeSave()` calls `expandCompositionLines()`; `expandCompositionLines()` static helper |
| `CreateOrder.php` | `mutateFormDataBeforeCreate()` calls `EditOrder::expandCompositionLines()` |

### `expandCompositionLines()` logic
```
foreach products:
  if mode != 'components' → keep line as-is
  else:
    load parent product + compositions + componentProducts
    for each composition:
      create a new line with:
        product_id  = component.id
        product_qty = parent_qty × composition.quantity
        uom_id      = component.uom_id
        price_unit  = component.cost
        price_subtotal = qty × price × (1 - discount/100)
    discard the original parent line
```

---

## Migrations

| File | Purpose |
|------|---------|
| `plugins/webkul/products/database/migrations/2026_04_28_000001_create_products_product_compositions_table.php` | Creates the compositions table |
| `plugins/webkul/products/database/migrations/2026_04_28_000002_add_is_composition_to_products_products_table.php` | Adds `is_composition` column |

---

## Linnworks Comparison

| Linnworks concept | Cydekick equivalent |
|-------------------|---------------------|
| Composite item    | Product with `is_composition = true` |
| Component         | `ProductComposition` row with `component_product_id` + `quantity` |
| "Sell as kit"     | `_composition_mode = 'parent'` on PO |
| "Expand on order" | `_composition_mode = 'components'` on PO |
| Stock depletion   | Not yet implemented — future: selling a composition depletes component stock |

### Key difference from Linnworks
Linnworks automatically depletes component stock when a composition is sold. In Cydekick, stock is managed per-product via `inventories_product_quantities`. Future work would add a sales order hook: when a composition is fulfilled, decrement each component's `ProductQuantity` rather than the parent's.
