# ERP: target-amount purchase order builder

Built 27-Aug-2026 in `erp.aradhyaenterprise.com` (Laravel + Livewire). **Written to disk, not committed** — the device bridge dropped before the commit step.

Brings the budget-first ordering workflow into the ERP: tick the items you need, name an amount, get an editable proposal, then save the PO and download the supplier's sheet in one press.

## What was added

| Piece | Role |
|---|---|
| `OrderPlannerService` | Allocates full boxes across the chosen items to land on a GST-inclusive target |
| `SmartOrderBuilder` (`/purchases/smart`) | Supplier + warehouse + target, searchable product picker, editable preview table, one button that saves the PO and downloads the Excel |
| `PurchaseOrderExport` | The sheet in 7 Days Organic's own rate-list layout (S.NO / PRODUCT NAME / UNIT / BOX QTY / MRP / RATE / ORDER / VALUE), static values not formulas |
| `PurchaseOrderExportController` (`/purchases/{po}/excel`) | Re-exports a saved order, rebuilt from its current lines so edits are reflected |
| `PurchaseService::updatePurchaseOrder` | Amends an open order under a row lock; refuses once goods are received |
| `PurchaseOrderEdit` (`/purchases/{po}/edit`) | Edit screen: quantities with box steppers, unit costs, add/remove lines |

Buttons added to the purchase index (Smart Order) and the PO show page (Download Excel, Edit). No migration — the existing `purchase_orders` / `purchase_order_items` schema carries everything.

## The allocation rule

Three constraints, in priority order:

1. **Full boxes only.** The supplier ships cartons; a part-box quantity is not an order they will pick.
2. **Every listed item appears.** Landing nearer the target by dropping a requested product is the wrong trade — the list is the requirement, the amount is the constraint.
3. **The money spreads across the list.** A buyer who names ten items wants ten items.

The algorithm seeds one box each, then repeatedly buys one more box of whichever product has had the *least money spent on it so far* and still fits under the target. A final step adds one last box if that lands nearer the target than stopping short.

**The first draft got rule 3 wrong and it was caught by testing, not by reading.** That version filled proportionally and then closed the gap with whichever box reduced the distance most. Best-fit closes a large gap fastest by repeatedly buying the largest carton, so on the real ten-item list it put **36.6% of a ₹2.25 L budget on Under Eye Cream alone** (3 boxes of 140) — a plan the buyer would have undone by hand immediately. Choosing by least-funded instead drops the largest line to 13.8% and, incidentally, lands closer: variance −₹279 against ₹2,25,000, versus +₹1,757 before.

Constraining the spread step to never cross the target also restored a guarantee the balanced version had lost: across 722 randomised affordable scenarios the plan now lands within **half the cheapest box's cost** of the target, worst case, with zero exceptions. The unconstrained balanced version was off by up to 16.9× a box.

## One pricing defect this works around

`products.purchase_price` holds the rate list's `SS-with gst` column — GST-**inclusive**. `PurchaseService` stores `unit_cost` tax-**exclusive** and adds `tax_rate` on top. The existing `PurchaseOrderForm` prefills `unit_cost` straight from `purchase_price`, so **every PO created through that screen is inflated by its own GST rate** — the same defect class as the sales-side bug fixed on 26-Aug. The new builder converts (`164.67 / 1.18 = 139.55`, matching the Rate column on the supplier's own PI 297 exactly). The old form was left alone; fixing it is a separate change.

## Verification status

- The allocation algorithm was reimplemented independently in Python and run against the real ten-item list plus 800 randomised scenarios. Whole boxes, every item kept, deterministic under input reordering, and the variance bound above — all hold.
- The GST conversion reproduces the supplier's own proforma figures.
- `php -l` passes on every file that could be reached (`OrderPlannerService`, `PurchaseOrderExport`, `PurchaseOrderEdit`, `PurchaseOrderExportController`).
- **`php artisan test` has not been run.** Neither sandbox has PHP with the project's vendor tree, and the bridge dropped before a commit. `tests/Feature/SmartPurchaseOrderTest.php` (16 tests) is written and syntax-checked but unexecuted.

## Before this goes live

1. **Replace `app/Services/OrderPlannerService.php`** with the corrected version — the copy on disk is the first draft with the lopsided allocation described above.
2. **Delete `_src_snapshot.tar.gz`** from the repo root — a 539 KB temp archive that could not be removed remotely.
3. **`php artisan test`** — run the suite, `SmartPurchaseOrderTest` above all.
4. **`npm run build`** — two new Blade views and edits to two more; `public/build` is a compiled snapshot.
5. **Check `products.purchase_price` and `box_quantity` are populated** — a product missing either is skipped by the planner or falls back to single pieces.
6. Commit. Pushing `master` triggers the GitLab CI production deploy, so hold the push until the suite is green.

## Not implemented

Supplier-specific rate lists (the planner reads one `purchase_price` per product); a printable PO PDF; re-planning that preserves hand edits (re-plan replaces the working lines wholesale, deliberately).  