# WP1 report — products screen toll-customer filter now reaches the API

**Status:** DONE
**Branch:** `hazemdev2` · **Commit:** `6e34b024a` (not pushed, not merged)
**Files changed:** 1 — `src/app/core/services/product.service.ts` (+4 lines, 0 removed)
**Backend:** untouched, as briefed.

## The change

`ProductService.list()` serialised exactly six filter keys; `toll_customer_id` was declared on the
`ProductFilters` interface but never read, so `buildFilters()`'s value was silently discarded and the
outgoing request was byte-identical to "no filter".

Added, immediately after the `track_inventory` line (now line 86-89):

```ts
// ISS-2026-9357 — the value is EITHER a partner id OR the literal `'own'`; both go to the
// server verbatim. Guarded on presence rather than truthiness so nothing is coerced, and an
// absent/cleared filter omits the key entirely instead of sending an empty value.
if (filters.toll_customer_id != null && filters.toll_customer_id !== '') params = params.set('toll_customer_id', String(filters.toll_customer_id));
```

### Which guard, and why

`!= null && !== ''` — not the surrounding truthiness style.

- Truthiness *would* work today (`'own'` is truthy, a partner id is never `0`), but it makes the fix
  depend on a property of the sentinel string rather than on presence. `!= null` states the actual
  rule: "a filter was chosen → send it".
- This mirrors `stock-balance.service.ts:125`, which deliberately uses `!= null` for the comparable
  owner-lens case (there the "own" sentinel *is* `0`, so truthiness would have been an outright bug).
- The extra `!== ''` is what satisfies acceptance criterion 4 strictly: a cleared select must omit the
  parameter, never send `toll_customer_id=`. `!= null` alone would let an empty string through.
- `String(...)` is a formatting call, not a coercion of kind — `'own'` stays `'own'`, `42` becomes
  `'42'`, which is what a query string is. Consistent with every neighbouring line.

## Acceptance criteria

### 1. Red-before-green — method used, stated plainly

This repo has **no test runner** (`skipTests: true`, no karma/jest/vitest), so this is **not** a unit
test and I am not claiming one. What I did instead:

I bundled the **real, unmodified `product.service.ts`** with the repo's own esbuild
(`./node_modules/.bin/esbuild wp1-harness.ts --bundle --format=esm --platform=node`) into a throwaway
Node script, instantiated `ProductService` via `Object.create(ProductService.prototype)` — which skips
the constructor, so `inject(HttpClient)` never runs — stubbed `svc.http.get` to capture the `opts.params`
object, called the **actual `list()` method body**, and printed the real `HttpParams.toString()`.

So the serialisation under test is the genuine production code path from `list()` through Angular's real
`HttpParams`. What is simulated is only the caller and the transport: the values are the ones
`buildFilters()` produces, hand-fed rather than clicked, and no request left a browser. I did not open
the app in a browser and did not inspect a live network tab.

**RED — before the edit:**

```
numeric id 42 : page=1&per_page=100
literal 'own' : page=1&per_page=100
cleared (none): page=1&per_page=100
explicit null : page=1&per_page=100
with siblings : page=1&per_page=100&search=abc&type=raw&product_category_id=3&is_active=true&track_inventory=false
```

**GREEN — after the edit, same harness, same inputs:**

```
numeric id 42 : page=1&per_page=100&toll_customer_id=42
literal 'own' : page=1&per_page=100&toll_customer_id=own
cleared (none): page=1&per_page=100
explicit null : page=1&per_page=100
with siblings : page=1&per_page=100&search=abc&type=raw&product_category_id=3&is_active=true&track_inventory=false&toll_customer_id=own
```

The harness file (`wp1-harness.ts`, repo root) was **deleted** after the green run; it is not in the
commit and `git status` is clean apart from the one service file. The bundles live only in the session
scratchpad.

### 2. Numeric id → `toll_customer_id=42`. ✅ (line 1 of GREEN)

### 3. "Our own materials" → `toll_customer_id=own`, not `0` / `null` / omitted. ✅ (line 2 of GREEN)

### 4. Cleared filter omits the parameter entirely. ✅ — both the `{}` case (`buildFilters()` never sets
the key when `filterTollCustomer` is falsy, `products.component.ts:2579`) and the explicit-`null` case
produce a query string with no `toll_customer_id` at all, not an empty value.

### 5. Paging keeps the filter. ✅ — verified by reading, not by clicking:
`onPageChange` (`products.component.ts:2621-2626`) computes the page and calls
`loadPage(page, event.rows)`; `loadPage` (2583-2585) calls `buildFilters()` fresh on every invocation
and passes the result to `productService.list(page, perPage, filters)`. The filter state
(`filterTollCustomer`, field at line 205) is not reset anywhere in that path. No change was needed.

### 6. No other filter's behaviour changes. ✅ — the diff adds one `if` and a comment; it removes and
rewrites nothing. The "with siblings" harness line is byte-identical before and after up to the new
suffix, which demonstrates the five pre-existing keys serialise exactly as they did.

### 7. Build and typecheck. ✅
- `npx tsc --noEmit` → exit 0, no output, no new errors.
- `npx ng build --base-href /app/` → **green**, output at `dist/moon-erp`. Only the pre-existing
  CommonJS-bailout warnings (`file-saver`, `html2canvas`/`jspdf`), unrelated to this change.

## Scope discipline

Diff is **one file, +4 lines**. WP2's nine other filter defects on this screen were left exactly as
found — no neighbouring line was tidied, no unrelated guard was "improved", no i18n file was touched
(no new key was needed). `git checkout` / `restore` / `stash` were never run on anything.

## Housekeeping

- `chown moonui2:moonui2` applied to the edited file.
- **Not deployed to `/app`** — the orchestrator deploys. Note the build output in
  `dist/moon-erp/browser/` is current as of this commit and ready to copy.
- Not pushed, not merged.

## Concerns

None blocking. Two things worth the orchestrator's awareness:

1. **The fix is unverified against a live server.** Everything above proves the correct query string is
   built; nobody has yet watched the backend return a filtered page. The brief states
   `ProductService::search()` already handles both `<id>` and `own`, and I did not re-verify the backend
   (out of scope). One click on the live screen after deploy closes this.
2. **The `ProductFilters.toll_customer_id` type is `number | string | null`,** so a future caller could
   pass any string. The guard sends whatever it is given verbatim, which is the requirement here; the
   backend is the validator. Not something to change in this WP.
