# WP1 — schema, status and relation

**Flags:** `migration: yes` · schema change → advisor gate.
**Repo:** BE `/home/moonui2/moon-erp-be`, branch `hazemdev2`.

## Goal

Lay the foundation for partial conversion of a purchase request into several purchase
orders — **without changing a single observable behaviour**. After this package the
database can express "how much of this request line has been ordered so far" and "which
request line did this order line come from", and the status enum has a value for a request
that is partly converted. Nothing reads or writes the new columns yet; that is WP3.

## Background you need

Today a purchase request converts **all** its lines at full quantity in one shot, and the
request is then frozen at `Converted` pointing at a single order through an unconstrained
integer column. There is no line-level tracking of any kind.

The pattern to copy already exists twice in this same module — do not invent a new one:

- `purchase_grn_items.purchase_order_item_id` — migration
  `Modules/Purchases/database/migrations/2026_02_25_300002_create_purchase_grn_items_table.php:14`,
  with an index at `:43`.
- `purchase_bill_items.purchase_order_item_id` — `…2026_02_25_400002_create_purchase_bill_items_table.php:14`.
- Cumulative counters on the **parent** line: `purchase_order_items.received_quantity` /
  `billed_quantity` — `…2026_02_25_200002_create_purchase_order_items_table.php:26-27`,
  rolled up by `PurchaseOrder::recalculateReceiveStatus()` —
  `Modules/Purchases/app/Models/PurchaseOrder.php:199-222`.

Read those three before writing the migration.

## Exact changes

### 1. One new migration (timestamp must sort after every existing one)

- `purchase_request_items.converted_quantity` — `decimal(15,3)`, NOT NULL, default `0`.
  Mirror the precision of `purchase_request_items.quantity` in
  `…2026_02_25_100002_create_purchase_request_items_table.php:20` — match it exactly.
- `purchase_order_items.purchase_request_item_id` — nullable `foreignId`, constrained to
  `purchase_request_items`, `nullOnDelete()`, **plus an index** (follow the GRN precedent).
- `purchase_requests.converted_at` — nullable `timestamp`.
- An index on `purchase_orders.purchase_request_id` (the column exists at
  `…2026_02_25_200001_create_purchase_orders_table.php:21` but the table's indexes at
  `:50-54` do not cover it, and WP3 will query it as a `hasMany`).

Write a working `down()`. Dropping a foreign key and its column in the same `down()` needs
`dropForeign` before `dropColumn` on MySQL — check how other migrations in this module do it.

### 2. `Modules/Purchases/app/Enums/PurchaseRequestStatus.php`

Add `case PartiallyConverted = 'partially_converted';`.

Then audit **every** method in the enum and decide deliberately (the file is only ~60 lines
— read all of it):

- `canConvert()` (`:46-49`) currently returns `$this === self::Approved`. It must now also
  allow `PartiallyConverted` — that is the whole point of the feature.
- `canCancel()` (`:51-54`), `isEditable()` (`:26-29`), `canSubmit()` (`:31-34`) — state in a
  code comment what you decided for the new case and why. A partially-converted request has
  real orders against it, so it should **not** become editable.

### 3. `Modules/Purchases/app/Models/PurchaseRequest.php`

- Add `converted_at` to `$fillable` (`:24-47`) and cast it to `datetime`. **This is a live
  bug fix:** both call sites already write `'converted_at' => now()`
  (`PurchaseOrderController.php:175` and `:562`) against a column that has never existed and
  a key that is not fillable, so Eloquent discards it silently — there is no conversion
  timestamp anywhere in the system today.
- Add `purchaseOrders(): HasMany` → `PurchaseOrder::class, 'purchase_request_id'`. Keep the
  existing scalar `converted_to_order_id` untouched; WP3 decides its fate.

### 4. `Modules/Purchases/app/Models/PurchaseRequestItem.php`

Add `converted_quantity` to `$fillable` + cast `decimal:3`, matching how `quantity` is cast.

### 5. `Modules/Purchases/app/Models/PurchaseOrderItem.php`

Add `purchase_request_item_id` to `$fillable`.

## Acceptance criteria

1. `php artisan migrate` runs clean on `moonui2_dev_be` **in this same step** (dev BE runs
   live — skipping it gives everyone 1054 errors). ⛔ never `migrate:fresh`/`refresh`/`db:wipe`.
2. `PurchaseRequestStatus::PartiallyConverted` exists and `canConvert()` is true for both
   `Approved` and `PartiallyConverted`.
3. `converted_at` now actually persists: a test that sets it and re-reads the model gets a
   non-null value.
4. **No behaviour change anywhere else.** Converting a request still converts everything and
   still lands on `Converted`. The existing tests at
   `Modules/Purchases/tests/Feature/PurchaseOrderApiTest.php:575` and `:618` must pass
   untouched — do not edit them.
5. New columns default such that every existing row reads as "nothing converted yet"
   (`converted_quantity = 0`).

## Tests

New file `Modules/Purchases/tests/Feature/PartialConversionSchemaTest.php`:

- the four columns/indexes exist (use `Schema::hasColumn`);
- `converted_at` persists through a save+refresh (proves the fillable fix);
- `PurchaseRequestStatus::PartiallyConverted->canConvert()` is true, and `Approved` still is;
- `PurchaseRequest::purchaseOrders()` returns the orders whose `purchase_request_id` matches.

⛔ Prefix every top-level helper in that file with `pcs…` — Pest loads all test files into
one process and a duplicate top-level function is a fatal redeclare that kills the whole run
with zero output.

Run: `/opt/cpanel/ea-php82/root/usr/bin/php -d memory_limit=1G vendor/bin/pest Modules/Purchases/tests/Feature/PartialConversionSchemaTest.php Modules/Purchases/tests/Feature/PurchaseOrderApiTest.php`

## Out of scope

- Any change to `PurchaseOrderController` — no conversion logic, no validation, nothing.
- The setting (WP2), the feature (WP3), the back door (WP4), cancellation (WP5), the UI (WP6).
- Do not touch `converted_to_order_id` semantics.
- No CHANGELOG bullet — this package is invisible to users.

## Finish

`./vendor/bin/pint` on touched files only · `chown moonui2:moonui2` every file ·
`bash local-deploy.sh` · conventional commit on `hazemdev2` · **do not push, do not merge**.
Commit as soon as the tests are green rather than batching — agents here have lost their
shell mid-run and left work uncommitted.
