# Task 2 report — toll customer column on the stock issue (إذن صرف)

**Date:** 2026-08-08 · **Branch:** `hazemdev2` (both repos, verified before committing) · **Not pushed, not merged.**

Owner ask (verbatim): «كمان ضيف في اذن الصرف عمود جديد فيه اسمه عميل التول لاذن الصرف ويظهر الوسم
التشغيلي او امر الانتاج الي مصروف عليها ان وجد».

---

## Commits

| Repo | SHA | Subject |
|---|---|---|
| BE `/home/moonui2/moon-erp-be` | `d9ef7e8ac` | `feat(inventory): expose the line product's toll customer on the stock issue` |
| FE `/home/moonui2/public_html/moon-erp` | `6f9d2d3fa` | `feat(stock-issues): add the «عميل التصنيع» (toll customer) column to the line grid` |

The bilingual `[Unreleased]` MoonStack bullet is in the BE commit
(`docs/moonstack/CHANGELOG.md`) — the FE repo has no changelog of its own; its `pre-push` hook only
nudges toward the BE one.

---

## Backend

**`Modules/Inventory/app/Http/Resources/InventoryIssueItemResource.php`**
Two new keys per line, mirroring `ProductResource` exactly (`name_ar ?: name`, gated on
`whenLoaded('product')`):

- `toll_customer_id` — `$this->product?->toll_customer_id`
- `toll_customer_name` — `$this->product?->tollCustomer?->name_ar ?: …->name`

The comment on the block states explicitly that this is **not** the line's `owner_partner_id`: that
is the consignment custody owner of the physical quantity, this is the owner of the catalogue
definition, and the two can differ on the same row.

**`Modules/Inventory/app/Http/Controllers/InventoryIssueController.php`** — every one of the **7**
sites that eager-loads `items.product` now also loads `items.product.tollCustomer:id,name,name_ar`
(lines 159, 179, 233, 315, 351, 405, 446: store / show / update / approve / cancel / partial paths).
So a whole document costs **one** extra query, not one per line. `index()` (line 78) loads no items
at all, so the list endpoint is untouched and pays nothing.

**`Modules/Core/app/Http/Controllers/ProductController.php`** — one line, and it is required for the
column to work rather than a drive-by: `show()` now eager-loads `tollCustomer:id,name,name_ar`
exactly as `index()` already did (line 73). The grid picks a product from the **search** response
(which carries the name) and then re-reads it through `products/{id}` to hydrate units / variants /
tracking; `loadProductMeta` **replaces** the cache entry with that response
(`stock-issues.component.ts:1378`), so without this the label appeared and was immediately blanked.
A regression test pins it.

### Deliberately not changed

`Modules/Sales/app/Http/Controllers/SalesInvoiceController.php:591` builds an `InventoryIssueResource`
after loading `items` **without** `items.product`. `product_name` and `product_code` are already
absent from that response today; the two new keys are absent there for the same reason and by the
same rule. Nothing regressed, and it is the deliver-from-invoice response, not a screen that shows
the issue grid.

---

## Frontend

- **`core/services/doc-config.service.ts`** — new `LINE('toll_customer', 'PRODUCTS.TOLL_CUSTOMER',
  { defaultVisible: true })` on `inventory.issue`, placed straight after `product_variant_id` (it
  describes the product). **Not locked**, so `columnToggles` — which is built from the registry by
  filtering `area === 'line' && !f.locked` — picks it up automatically and Settings → Documents can
  hide it. Companies that already have a persisted column order keep theirs and gain the new column
  appended at the end: `TxColumnLayout.orderedColumns()` puts keys absent from the saved order after
  the ordered ones (`tx-column-layout.ts:103-108`). Visibility for a company with saved overrides
  falls through to `defaultVisible !== false` (`doc-config.service.ts:496-502`), so it shows up.
- **`features/stock-issues/stock-issues.component.html`** — header `@case` and cell `@case` added in
  the *same* `colLayout.orderedColumns()` loops as every other column (the `@default { <td></td> }`
  makes a forgotten cell case fail silently as a blank column, so both were added). The cell is
  read-only text: `{{ lineTollCustomer(item.get('product_id')?.value) || '—' }}`.
- **`stock-issues.component.ts`** — `toll_customer: '150px'` in the default-width map; new
  `lineTollCustomer(productId)` helper reading the same `products()` cache the barcode / variant /
  base-unit helpers use; and the synthetic products seeded when an existing issue is opened for edit
  now carry `toll_customer_id` / `toll_customer_name` straight from the API line, so the column is
  populated on the first paint instead of depending on the per-line `products/{id}` round-trip.
- **`stock-issues.component.scss`** — `.toll-customer-cell` (ellipsis under `table-layout: fixed`)
  plus a dark-mode colour.
- **`core/models/inventory.model.ts`** — `IssueItem` gains the two optional fields.

**i18n: no new key.** `PRODUCTS.TOLL_CUSTOMER` already exists in **both** `ar.json` and `en.json`
(«عميل التصنيع» / “Toll customer”) and is what the products screen and the stock-balances screen
already call this concept. The owner said «عميل التول»; using the app-wide label keeps one name for
one thing. **If he wants the literal «عميل التول» on screen, that is a one-word edit to the existing
`PRODUCTS.TOLL_CUSTOMER` value in `ar.json` — say the word and it changes everywhere at once.**
No i18n file was checked out, restored or stashed at any point.

---

## Tests, build, verification

| Gate | Result |
|---|---|
| `pest Modules/Inventory/tests/Feature/InventoryIssueTollCustomerTest.php` | **6 passed** (16 assertions) |
| `pest Modules/Inventory` | **794 passed, 4 failed** — all 4 pre-existing and unrelated (below) |
| `pest` on the 9 Core product/toll test files (`ProductApiTest`, `TollCustomerMaterialCodeTest`, …) — because `ProductController` was touched | **116 passed, 1 failed** — `ProductCategorySlugTest` “storefront category listing includes slug”, verified failing identically at `HEAD~1`; unrelated to `show()` |
| `./vendor/bin/pint` (touched files only) | pass |
| `npx tsc --noEmit` | clean, no output |
| `npx ng build --base-href /app/` | green (only the pre-existing `file-saver` / `html2canvas` CommonJS warnings) |
| Deploy to `/app` | **not** done, as instructed |
| `chown moonui2:moonui2` on every edited file + `bash local-deploy.sh` | done |

New test file `Modules/Inventory/tests/Feature/InventoryIssueTollCustomerTest.php`:

1. the line carries its product's toll customer, Arabic-first;
2. Latin-name fallback when `name_ar` is empty (`name_ar` is NOT NULL on `business_partners`, so
   "no Arabic name" is the empty string — the first draft used `null` and hit the constraint);
3. a company-owned material reports `null`/`null` (renders «—»);
4. a mixed document resolves each line independently;
5. **N+1 guard** — the number of `business_partners` queries for a six-line document equals that of
   a one-line document (a relative assertion, because the issue's own partner comes from the same
   table; the absolute count is not the point, the *scaling* is);
6. `products/{id}` returns `toll_customer_name` (the hydration path fixed above).

Every top-level helper is prefixed with the file's subject (`setupTenantForIssueTollCustomer`,
`makeIssueTollCustomer`, `makeIssueTollProduct`, `makeIssueWithTollLines`) — `Modules/Core`'s
`TollCustomerMaterialCodeTest.php:43` already declares a top-level `tollCustomer()`, and Pest loads
every file into one process.

**The 4 failures are pre-existing — proven, not assumed:**

- `LotAllocationTest` — “FEFO allocation consumes the earliest-expiry lot first…”, “a quantity
  beyond the owner lots leaves an unassigned shortfall…”
- `OpeningBalanceApiTest` — “opening balance creates inventory movement with type opening”
- `TransferLotPreservationTest` — “FEFO across two source lots preserves both batches…”

I checked the three source files out at `HEAD~1` (the commit *before* mine), re-ran those three test
files, and got **the same 4 failures, 27 passed** — then restored `HEAD`, re-`chown`ed and re-ran
`local-deploy.sh` (working tree verified clean afterwards). They are also structurally unreachable
from this change: none of the three test files contains a single reference to the issue endpoints or
to `products/` (`grep -c "issues\|products/"` → 0 on all three); they are FEFO / lot-balance /
costing assertions. **Zero new failures.**

### Verified against real dev data (read-only)

Only 2 of ~17,000 products on `moonui2_dev_be` carry a `toll_customer_id` (ids 17167 / 17168,
company 4), exactly as the brief warned — and exactly **one** existing issue line uses one of them.
Reading that document through the real resource:

```
issue GDN-000051  ref=["Job tag","اورد 1"]
  line: منتج | toll_id=52 | toll_name="ABDULLAH HAMED SALEEM AL SAHLY"
```

So on this DB the column will read «—» on virtually every row and the real name on `GDN-000051` —
that is the data, not a bug. No DB mutation of any kind was made (no `migrate:fresh`, no seeding, no
writes).

---

## The production order / job tag — verified, not rebuilt

All three places still render it, and none of them is on any code path this change touched:

| Place | Evidence |
|---|---|
| Issue **list** | `stock-issues.component.html:20` — `{{ item.reference_type_label \|\| item.reference_type }}` + `— {{ item.reference_number }}` |
| Issue **detail dialog** | `stock-issues.component.html:688` (was 683 before my two added `@case` blocks shifted it) — same pair |
| **Printout** | `stock-issues/stock-issue-print.config.ts:47-58` — pushes an «المرجع / Reference» extra field joining `reference_type_label` and `reference_number` |
| API | `InventoryIssueResource:30,34` — `reference_type_label` + `referenceNumber()`, which resolves a production order to its `order_number` and a job tag to its localized name |

Confirmed live on `GDN-000051` above: `reference_type_label = "Job tag"`, `reference_number = "اورد 1"`.

---

## Reported, not built (owner's call)

1. **The detail dialog's read-only item table has no toll-customer column.** The data is now in the
   item resource, so it is a 3-line addition — but that table is not driven by `TxColumnLayout` (it
   is a hand-written `<table>` whose serial/lot expansion rows and `<tfoot>` use hard-coded
   `colspan="8"` / `colspan="3"` values that would all have to shift). The brief scoped this task to
   the `TxColumnLayout` grid, so I did not guess at it. Worth noting the edit grid is the **draft**
   view; once an issue is approved the keeper reads the detail dialog, so if the owner's intent was
   "I want to see it on any issue I open", this is the follow-up he wants. Small and safe, just not
   free.
2. **The printout has no toll-customer column either** — same reasoning, plus a printed issue has a
   fixed column budget and adding one column costs another its width. Ask before adding.
3. Neither the **stock receipt** (إذن إضافة) nor the warehouse transfer shows a toll customer. Same
   `DOC_CONFIG_REGISTRY` mechanism, and the receipt already loads `items.product` — cheap to mirror
   if he asks. Out of scope here (and `stock-receipts/*` belonged to the parallel agent this
   session, so I stayed out of it entirely).

## Concerns

- **The label wording** — «عميل التصنيع» vs the owner's «عميل التول». Reused the existing key
  deliberately; flagged above so he can decide.
- **Query-count discipline is now load-bearing in two places.** `InventoryIssueItemResource` reads
  `$this->product?->tollCustomer` without a `relationLoaded` guard (deliberately — mirroring
  `ProductResource`, and a guard would turn a missed eager load into a silent wrong «—» instead of a
  visible slowdown). If someone adds an 8th `items.product` load site and forgets `tollCustomer`,
  it degrades to one query per line. The N+1 test covers `show()`, which is the path the grid uses.
