parity(docs): D16 — stop PRICE-01 as storefront, reorder Phase 1, verify SEC-02

Owner ruling: computing and displaying price / price_with_discount is retail
storefront work, so it falls under D13 and is marked out of scope. The B2B
app works in making charge only.

variant-price.ts and its 11 hand-checked tests are kept and still run, just
unused. Nothing needed reverting: computeVariantPrice was never called from
outside its own file and spec, verified by grep over apps/api/src and
apps/admin-panel/src, so no price had been wired into any screen. PRICE-01
is re-marked in ISSUES.md rather than deleted so the evidence survives if
the storefront is ever taken up.

Making-charge work stays fully in scope: percentage_pay,
makingChargeOffsetPercent, and wage display on variants and order lines.

New Phase 1 order: Persons -> products/categories/variants/order detail ->
financial documents -> users and permissions -> admin SMS -> promotions,
notifications, cities, branches, workshops -> reports -> gate. Per section:
implement the gaps, then field-by-field test that section, and show a gap
summary before fixing.

SEC-02 re-verified live rather than quoted: 9/9 assertions, plus 32/32 in
auth-smoke.sh. POST /orders is 401 without a token; a body customerId of
"customer-2" sent with a customer-dev-test token still lands the order on
customer-dev-test and is warn-logged; a foreign order id returns 404 rather
than 403 so ids stay unenumerable; the admin read rejects a customer token.
Client-supplied wagePercent/discountPercent/flatRialCharge remain as MON-08,
already scheduled as Phase 2 item #1.

Also records why the dev OTP log briefly looked broken: a stale API process
still held port 3077, so the new instance died with EADDRINUSE and the
tailed log belonged to the old process. Not a regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-09-22 16:43:11 +03:30
co-authored by Claude Opus 5
parent 64e76a8616
commit e3af31948e
5 changed files with 193 additions and 9 deletions
+18 -6
View File
@@ -169,9 +169,21 @@ To be filled, in the fix order given in the Phase 1 summary:
---
## §3 — Price formula and product pricing — **the top critical gap**
## §3 — Price formula and product pricing — ⏸️ STOPPED BY D16
### PRICE-01 — the target computes no product price at all ❌ CRITICAL
> **Owner ruling, 2026-09-22.** Computing and showing `price` /
> `price_with_discount` is a **retail-storefront** concern; the B2B app works in
> **making charge (اجرت)** only. PRICE-01 is therefore ⏸️ under D13 and is NOT
> worked on. `variant-price.ts` and its 11 tests are **kept but unused**;
> nothing needed reverting because the function was never wired in.
>
> **Making charge stays fully in scope** — `percentage_pay`,
> `makingChargeOffsetPercent`, and wage display on variants and order lines.
>
> The findings below are retained as evidence for whenever the storefront is
> taken up.
### PRICE-01 — the target computes no product price at all — ⏸️ out of scope
| Dimension | Reference | Target | Status |
|---|---|---|---|
@@ -183,15 +195,15 @@ To be filled, in the fix order given in the Phase 1 summary:
| Displayed in the product list | `henza_admin/src/views/apps/product/ProductList.vue:283,287,301` — `{{ item.price }}` in three places | no price column | ❌ missing |
| Extra percent source | `ConfigStatic['product_price_extra_percent']`, default 7 (`ProductVariant.php:50`) | the ported function defaults to 7 but nothing reads the config row | ❌ missing |
**Impact.** Every admin screen that shows a product price shows nothing. This is
the single largest behavioural gap in the admin panel and it is money-facing, so
it is **fix #1**.
**Impact (historical).** Every admin screen that would show a product price shows
nothing. Under D16 this is storefront behaviour, so it is not a Phase 1 gap.
**What is already done:** the formula itself is ported and proven — the live
version at `:183-190`, not the three superseded variants above it that apply the
9% to the whole price (a test asserts the difference: 39,986,000 vs 42,686,000).
**What remains:** read `product_price_extra_percent` from settings, fetch the
**What remains (only if the storefront is ever taken up):** read
`product_price_extra_percent` from settings, fetch the
current gold price, call `computeVariantPrice`, and surface `price` /
`price_with_discount` in `variantRow()` and the product list column.
+48
View File
@@ -398,3 +398,51 @@ reliably, 0 single-token, 0 empty, **1 needs review** — `customer-dev-test`
first name «محمد علی» survives intact, which is the defect the owner
originally reported.
- Typecheck: api ✅ · admin-panel ✅.
---
## 2026-09-22 (later still) — D16 priority change, SEC-02 re-verified
### PRICE-01 stopped (D16)
Price display is retail-storefront work, so it falls under D13 and is now ⏸️.
- `apps/api/src/modules/pricing/variant-price.ts` and its 11 tests are **kept**
and still run; they are simply not used.
- **Nothing needed reverting.** `computeVariantPrice` was never called from
anywhere outside its own file and spec — verified by grep over `apps/api/src`
and `apps/admin-panel/src`. No code had been wired to display a price.
- Making-charge work (`percentage_pay`, `makingChargeOffsetPercent`, wage display
on variants and order lines) **stays in scope**.
- `ISSUES.md` PRICE-01 re-marked ⏸️ rather than deleted, so the evidence survives.
### SEC-02 re-verified live
Re-tested end to end rather than quoting the earlier run:
| Check | Result |
|---|---|
| `POST /orders` with no token | 401 |
| Body `customerId: "customer-2"` + `customer-dev-test` token | landed on **customer-dev-test**; warn-logged |
| `GET /orders/:id` own | 200 |
| `GET /orders/:id` another customer's | **404** (not 403) |
| `GET /orders/:id` unknown | 404, identical |
| `GET /orders/admin/:id` with a customer token | 401 |
**9/9 passed**, plus **32/32** in `auth-smoke.sh`.
### Data deletions (standing rule 2)
| What | How many | Why |
|---|---|---|
| 1 `Order` created by the SEC-02 test, with its `OrderItem`, `OrderStatusEvent` and `LedgerEntry` rows | 1 order + its children | Test fixture, removed by the test itself. |
### A testing note worth keeping
The dev OTP line briefly appeared to be missing after the SMS change. It was not
a regression: a **stale API process from an earlier test still held port 3077**,
so the new instance died with `EADDRINUSE` and the log being tailed belonged to
the old process. Kill the listener (`netstat -ano | grep :3077`) before
concluding anything from a log. `smsConfigured()` returns false with no
credentials, so the dev log fires exactly as intended — confirmed once the port
was free.
+80
View File
@@ -878,3 +878,83 @@ three rows from both attempts were deleted and logged.
- Console output of Persian is not evidence: the Windows console renders it as
`?` even when the data is correct. Verify by comparing values or codepoints in
Node, as `scripts/test-name-trigger.cjs` does.
---
# D16 — Price display is storefront; new work order (2026-09-22)
## 1. PRICE-01 is stopped — price belongs to the retail storefront
**Owner's ruling.** Computing and displaying `price` / `price_with_discount` is a
**retail storefront (تک‌فروشی)** concern. The B2B app works in **making charge
(اجرت)** only. So PRICE-01 falls under D13 and is marked **⏸️ preserved — out of
current scope**.
| Item | Ruling |
|---|---|
| `apps/api/src/modules/pricing/variant-price.ts` | **Kept, not deleted.** Simply not used. |
| `variant-price.spec.ts` (11 hand-checked tests) | **Kept** and still runs in CI. |
| Any code wiring price into the admin panel | **Revert.** — *None existed: `computeVariantPrice` was never called from anywhere outside its own file and spec, so there was nothing to revert. Verified by grep over `apps/api/src` and `apps/admin-panel/src`.* |
| Making-charge fields — `percentage_pay`, `makingChargeOffsetPercent`, wage display | **Still fully in scope.** |
PRICE-01 in `ISSUES.md` is re-marked ⏸️ rather than deleted, so the analysis and
its evidence stay available if the storefront is ever taken up.
**Why this matters beyond one row:** the reference's `ProductVariant::$appends`
carries `price`, `color_str` and `price_with_discount` together
(`ProductVariant.php:115-119`). Only the price pair is storefront; anything else
appended there is still judged on its own merits.
## 2. The gate stands: admin first, then the apps
### Phase 1 — admin, in this order
**(a) Persons (اشخاص)**
1. The **5 missing sales-expert fields** (`ncode`, `phone`, `city_id`,
`gram_level`, `reagent_id`): **report first** — where the reference uses each
one for `type=1` — then add according to what that shows. Standing proposal:
a `SalesExpertProfile` related to `AdminUser` (D10). Fields with no use are
**listed for the owner to decide**, not added speculatively.
2. `singleCustomer` migrates to **diff-send** (D11).
3. Field-by-field audit of every form and grid in Persons — retail customer,
wholesale customer, sales expert — per D12, each with its round-trip test.
**(b) Products, categories, variants, and order detail in the admin** — deep
field-by-field: every form field, column, filter and button (D12).
- The **variant image picker stays untouched** (PROTECTED, `PROTECTED.md` §B);
only genuine omissions are added.
- The **making charge of every variant and every order line must display and
persist exactly as the reference does.**
**(c) The rest of the admin, in order:** financial documents → users and
permissions → admin SMS templates → promotions / notifications / cities /
branches / workshops → reports.
**(d)** Full admin test pass, the Phase 1 report, then the gate question
**«ادمین کامل و تست شد. بریم سراغ اپ‌ها؟»** — and **stop and wait**.
### Phase 2 — apps (only after the owner approves)
**(a)** The 5 v2-behind-v1 logic gaps, most important the **cart stock ceiling
(`capQty`/`maxQty`)**, matching v1 and the reference exactly — v2's design fully
preserved, with a test proving v1 and v2 behave identically.
**(b)** The missing screens (`report`, `report/detail`, `ChildOrder`,
`customerChoice`, `complete_profile`) in **both** apps, each in its own design,
plus whatever backend they need.
**(c)** Field-by-field audit of products, categories, variants, cart and order in
the apps.
**(d)** The remaining Phase 2 items: MON-08, removing `customerId` from the order
DTO, route analytics, the `isLoaded` / `sts_verify_step_two` guards, guest
behaviour, app SMS templates, and the remaining screens.
## 3. Working method per section
**Implement the gaps first, then field-by-field test that same section.** Before
fixing any section, **show the owner a short summary of the gaps found** and wait.
This supersedes the "audit everything, then fix everything" reading of D12: the
field-by-field requirement is unchanged, but it is applied **per section**, with
a checkpoint before each one.
+1 -1
View File
@@ -123,6 +123,6 @@ Severity: `Critical` (money loss / security / data loss) · `High` · `Medium`
| ID | Severity | Where | Description | Evidence | Status |
|---|---|---|---|---|---|
| PRICE-01 | **Critical** | TGT `apps/api/src/modules/admin-products/admin-products.service.ts:188-208`; `apps/admin-panel/src/views/apps/product/ProductList.vue` | **No product price is computed or shown anywhere in the admin panel.** The reference appends `price` and `price_with_discount` to every variant (`ProductVariant.php:115-119`), sets the live gold rate before listing (`AdminController.php:97-98`), and renders `{{ item.price }}` in three places in its product list (`ProductList.vue:283,287,301`). The target's `variantRow()` returns neither field, nothing fetches a gold price for the admin list, and `computeVariantPrice()` — ported and unit-tested today — is called from nowhere. | `grep computeVariantPrice src/ --include=*.ts` matches only its own file and spec. | Open — **fix #1 of Phase 1**. |
| PRICE-01 | ⏸️ out of scope (was Critical) | TGT `apps/api/src/modules/admin-products/admin-products.service.ts:188-208`; `apps/admin-panel/src/views/apps/product/ProductList.vue` | **No product price is computed or shown anywhere in the admin panel.** The reference appends `price` and `price_with_discount` to every variant (`ProductVariant.php:115-119`), sets the live gold rate before listing (`AdminController.php:97-98`), and renders `{{ item.price }}` in three places in its product list (`ProductList.vue:283,287,301`). The target's `variantRow()` returns neither field, nothing fetches a gold price for the admin list, and `computeVariantPrice()` — ported and unit-tested today — is called from nowhere. | `grep computeVariantPrice src/ --include=*.ts` matches only its own file and spec. | ⏸️ **Stopped by D16** — price display is retail-storefront, out of scope. `variant-price.ts` and its 11 tests are KEPT but unused. Nothing needed reverting: the function was never called. Making-charge fields stay in scope. |
| PRICE-02 | Info | REF `app/Models/ProductVariant.php:163` vs TGT `admin-products.service.ts:206-208` | `rial_pay` (a flat rial charge per variant) appears only in the **superseded, commented-out** formula, not the live one at `:183-190`. The target returns null rather than faking it, which matches the live reference. Recorded so nobody "restores" it from the dead code. | Read both. | ✅ No action — matches. |
| PERM-01 | Medium | TGT `prisma/seed.ts`; `apps/admin-panel/src/views/user/permissionConst.ts` | `dashboard.omde` and `dashboard.tak` are seeded into the `permission` table but are **absent from the permission UI tree**, so they can be held by a row yet never seen, granted or revoked from the admin screen — and nothing reads them any more (the protected Dashboard uses `dashboard.view`). The DB holds **178** distinct rids against the UI's **174**. | `SELECT rid FROM permission GROUP BY rid` → 178, including all three dashboard rids. | Open — the fix removes permission rows, which is a data deletion and needs the owner's approval first. |
+46 -2
View File
@@ -10,7 +10,7 @@
| **Phase** | **Phase 1 — admin parity, matrix in progress** |
| **Branch** | `parity/phase-1-admin` (cut from `parity/security-hotfix`) |
| **Status** | Both stages done and verified live: **46/46** smoke assertions, **7/7** jest unit tests, typecheck + build clean on all three projects. |
| **Next step** | Owner is reviewing the matrix summary; §3 PRICE-01 is fix #1 once the order is confirmed |
| **Next step** | Phase 1 (a) Persons — report where the reference uses the 5 sales-expert fields for `type=1`, then implement per D16 |
| **Blocked on** | nothing |
| **Last updated** | 2026-09-22 |
@@ -51,7 +51,7 @@ while the id and the id-addressed routes remain.**
## ⚠️ Read `DECISIONS.md` before doing anything
All user rulings live there (D0–D15). Highlights that change how work is done:
All user rulings live there (D0–D16). Highlights that change how work is done:
- **D12 — the matrix is FIELD-BY-FIELD, not page-by-page**, and a 4-step
round-trip test per entity is mandatory. Both rules were added to the
@@ -105,6 +105,7 @@ All user rulings live there (D0–D15). Highlights that change how work is done:
| D13 | **Retail storefront (wallet, cashback, `ConfigStatic['percent']`) preserved but OUT OF SCOPE.** Nothing deleted or disabled; no parity work; matrices list it ⏸️ with no gap/work row; ask before touching shared surfaces. |
| D14 | **`migrate dev` in development, `migrate deploy` in production and nowhere else.** Never `migrate reset`; never edit `_prisma_migrations`. On drift + a reset offer: **stop and report**. |
| D15 | **`_goldbank_migrations` is KEPT** — it is the tracking table of the app's own migration runner (`src/prisma/migrate.ts`), needed because the Docker image has no Prisma CLI. Declared `@@ignore`d; drift is zero. **Persian test input must come from a FILE, never a shell argument.** |
| D16 | **PRICE-01 STOPPED** — price display is retail-storefront (⏸️ D13); the B2B app works in making charge only. `variant-price.ts` + tests kept but unused. **Making charge stays in scope.** New work order: Persons → Products/variants/order-detail → the rest of the admin → gate → apps. **Per section: implement gaps, then field-by-field test, and show a gap summary BEFORE fixing.** |
## Open — awaiting user approval
@@ -215,3 +216,46 @@ The Windows shell replaces Persian characters with `?`. A test that passes its
data inline can "pass" while storing `????`. Write fixtures in the test file or
a UTF-8 JSON file and send bodies with `curl --data-binary @file`. Console
output of Persian is never evidence — compare values in Node.
## ⚠️ Phase 1 work order (D16 — supersedes the earlier ordering)
**(a) Persons (اشخاص)** ← current
1. The 5 missing sales-expert fields (`ncode`, `phone`, `city_id`, `gram_level`,
`reagent_id`): **report first** where the reference uses each for `type=1`,
then add accordingly (proposal: `SalesExpertProfile` related to `AdminUser`,
D10). Unused fields are listed for the owner, not added speculatively.
2. `singleCustomer` → diff-send (D11).
3. Field-by-field audit of every Persons form and grid (retail, wholesale,
sales expert) per D12, each with its round-trip test.
**(b) Products, categories, variants, admin order detail** — deep field-by-field.
The variant image picker is PROTECTED (§B) — additive only. Making charge on
every variant and order line must display and persist exactly as the reference.
**(c) The rest, in order:** financial documents → users & permissions → admin SMS
templates → promotions / notifications / cities / branches / workshops → reports.
**(d)** Full admin test pass → Phase 1 report → gate question → **STOP and wait**.
### Method per section (D16)
Implement the gaps first, then field-by-field test that same section. **Show the
owner a short gap summary BEFORE fixing each section.**
## ✅ SEC-02 — verified complete (re-tested 2026-09-22)
| Check | Result |
|---|---|
| `POST /orders` without a token | 401 (was `@Public`) |
| Body `customerId: "customer-2"` with a `customer-dev-test` token | order landed on **customer-dev-test**; spoof blocked and warn-logged |
| `GET /orders/:id` own order | 200 |
| `GET /orders/:id` another customer's order | **404**, not 403 — ids stay unenumerable |
| `GET /orders/:id` unknown id | 404, identical response |
| `GET /orders/admin/:id` with a customer token | 401 |
**9/9 passed** live, plus **32/32** in `auth-smoke.sh`. The server derives the
customer from the token only (`orders.controller.ts:26-29`,
`orders.service.ts` `createOrder(dto, customerId)`).
Client-supplied `wagePercent` / `discountPercent` / `flatRialCharge` remain —
that is **MON-08**, scheduled as Phase 2 item #1 per the owner's ruling.