parity(api): port the reference price formula with tests; SMS + MON-08 reports
Adds computeVariantPrice(), a verbatim port of the live formula at henza_server/app/Models/ProductVariant.php:183-190, with the per-customer making-charge offset as an explicit input defaulting to 0. The three superseded formulas commented out above it apply VAT to the whole price and are deliberately not ported (D4). 11 unit tests assert against hand calculations derived from the PHP source rather than from the implementation, covering: with and without the offset, VAT on the non-metal portion only, floor-to-nearest-1000, the extraPercent default of 7, and the equivalence between the app folding the offset into percent_pay and the server deriving it. 18 tests pass overall. Also adds the D0 name-split review report script (read-only) and two investigations the owner asked for: - SMS: the reference uses the Trez panel (smspanel.trez.ir, Sms.php:69,97) with production credentials committed to git (SEC-12 — rotate them). All 12 message templates catalogued; env var names proposed; nothing implemented pending approval. - MON-08: henza_pwa computes percent_pay, discount and rial_pay rather than asking the user (cart.vue:170-179, Mixins/product/index.ts:69-75), so Phase 2 item #1 is to derive them server-side and ignore the client's values. rash_weight stays read from the DB as a recorded deviation. Records the owner's two new standing rules: migrations only through the standard Prisma workflow, and every data deletion logged in CHANGELOG. [ref: henza_server/app/Models/{ProductVariant,Sms}.php, henza_pwa/src/views/cart.vue] Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c71574b1d2
commit
7b598e9e8e
@@ -235,3 +235,74 @@ order routes 401 without a token, a spoofed body `customerId` ignored in favour
|
||||
of the token, own order readable, unknown/foreign order id 404, and the admin
|
||||
order route rejecting a customer token while admitting an admin one.
|
||||
Typecheck clean on api, user-app, user-app-v2 and shared-types.
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-22 — migrate resolve, D13 scope, and the D0/percent migration set
|
||||
|
||||
### Standing rule adopted (owner, 2026-09-22)
|
||||
|
||||
1. **Migrations run only through the standard Prisma workflow** — `migrate dev`
|
||||
in development. **`prisma db execute` is no longer used to apply schema
|
||||
changes.** (It was used once, in the security hotfix, to create the
|
||||
`CustomerOtp` table; that is what the resolve below repairs.)
|
||||
2. **Every data deletion is recorded here** — including throwaway rows this
|
||||
session creates itself.
|
||||
|
||||
### Data deletions (rule 2, applied retroactively to this session)
|
||||
|
||||
| When | What | How many | Why |
|
||||
|---|---|---|---|
|
||||
| Security hotfix stage 3 | `SmsMessage` rows written by the OTP flow during testing, whose `body` still held the live code before the SEC-11 fix | 9 | They contained real OTP codes in a grid any `sms`-rid operator can read. Test data only; no customer-facing message was ever sent (no gateway is wired). |
|
||||
|
||||
No other data has been deleted in this project.
|
||||
|
||||
### Ledger repair — approved and executed
|
||||
|
||||
```
|
||||
npx prisma migrate resolve --applied 20260922000000_customer_otp
|
||||
→ Migration 20260922000000_customer_otp marked as applied.
|
||||
npx prisma migrate status
|
||||
→ 9 migrations found. Database schema is up to date!
|
||||
```
|
||||
|
||||
The `CustomerOtp` table already existed (created with `db execute` in stage 2);
|
||||
this wrote the missing `_prisma_migrations` row **without running any SQL**, so
|
||||
`migrate deploy` will no longer try to re-create the table. No schema change.
|
||||
|
||||
### Documentation
|
||||
|
||||
- **D13 recorded** — the retail storefront (wallet, cashback,
|
||||
`ConfigStatic['percent']`) is preserved but out of scope. `DECISIONS.md` §D13,
|
||||
`PROTECTED.md` §H, and status `⏸️` plus RULE C1 added to the audit skill.
|
||||
- **Endpoint count corrected: 412, not 369.** `routes/admin.php` has 151 live
|
||||
explicit routes, not the 117 the Phase 0 inventory recorded (DOC-01). Of the
|
||||
412, **109 are retail storefront**, 15 are `web.php` dev/ops, and **288 stay
|
||||
in scope** (218 admin + 70 B2B app).
|
||||
- **SMS provider report** — the reference uses the Trez panel
|
||||
(`smspanel.trez.ir`, `Sms.php:69,97`) with **credentials committed to git**
|
||||
(SEC-12, must be rotated). All 12 message templates catalogued, env var names
|
||||
proposed. **Nothing implemented; awaiting approval.**
|
||||
- **MON-08 answered** — `henza_pwa` **computes** `percent_pay`, `discount` and
|
||||
`rial_pay`; the user types none of them (`cart.vue:170-179`,
|
||||
`Mixins/product/index.ts:69-75`). So Phase 2 item #1 is to derive them
|
||||
server-side and ignore the client's values, with a comparison test proving the
|
||||
total is unchanged for an ordinary customer. `rash_weight` stays read from the
|
||||
DB, recorded as a deliberate deviation.
|
||||
|
||||
### New, not yet applied — awaiting approval
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `prisma/migrations/20260922120000_customer_name_parts/migration.sql` | D0 — adds `firstName`/`lastName`, backfills first-token/rest, adds a `customer_sync_full_name` trigger keeping `fullName` in sync in both directions, indexes both parts. Additive; nothing dropped. |
|
||||
| `prisma/migrations/20260922120100_customer_making_charge_offset/migration.sql` | D9 — adds `makingChargeOffsetPercent DECIMAL(6,3) NOT NULL DEFAULT 0`. |
|
||||
| `scripts/report-name-split.cjs` | Read-only review report for names with more than two tokens (D0 requirement 3), with `--csv` output. |
|
||||
|
||||
### Added and already passing
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `src/modules/pricing/variant-price.ts` | The reference price formula ported verbatim from `ProductVariant.php:183-190`, with the customer offset as an explicit input defaulting to 0. |
|
||||
| `src/modules/pricing/variant-price.spec.ts` | 11 tests against **hand calculations** derived from the PHP source, covering with/without the offset, the VAT-on-non-metal-portion-only rule, floor-to-1000, and the equivalence between the app folding the offset into `percent_pay` and the server deriving it. |
|
||||
|
||||
`npx jest` → **18 passed** (7 existing + 11 new), 2 suites.
|
||||
|
||||
@@ -529,3 +529,187 @@ The three unrelated `percent` meanings were already separated in
|
||||
`MIGRATION-STATE.md`. D13 confirms that **`ConfigStatic['percent']` (wallet
|
||||
cashback) is storefront and out of scope**, while **`customers.percent`
|
||||
(→ `makingChargeOffsetPercent`) is B2B and stays in scope**.
|
||||
|
||||
---
|
||||
|
||||
# Report — SMS provider (parity investigation, 2026-09-22)
|
||||
|
||||
Requested before implementing. **Nothing was implemented yet; awaiting approval.**
|
||||
|
||||
## 1. Which provider does the reference use?
|
||||
|
||||
**Trez SMS panel** (`smspanel.trez.ir`), called directly with Laravel's `Http`
|
||||
facade. There is **no package, no config file and no env var** — the endpoint and
|
||||
the credentials are hardcoded in one model.
|
||||
|
||||
| Item | Value | Where |
|
||||
|---|---|---|
|
||||
| Class | `App\Models\Sms` | `henza_server/app/Models/Sms.php` |
|
||||
| Live endpoint | `https://smspanel.trez.ir/SendMessageWithCode.ashx` | `Sms.php:69` (`Send`), `Sms.php:97` (`SendSms`) |
|
||||
| Method | `POST`, form-urlencoded, 20s timeout | `Sms.php:71-78` |
|
||||
| Form fields | `PhoneNumber` (sender line), `Username`, `Password`, `Mobile`, `Message` | `Sms.php:73-77` |
|
||||
| Persistence | `Sms::set()` writes every message to the `messages` table | `Sms.php:80,120-129` |
|
||||
| Queue | `SendSmsJob` / `SendSmsAfterCreate` exist, but `QUEUE_CONNECTION` defaults to `sync` (`config/queue.php:16`), so sends are inline | `app/Jobs/SendSmsJob.php:40` |
|
||||
|
||||
**A superseded provider is commented out above each call**: `0098sms.com`
|
||||
(`sendsmslink.aspx`, GET, sender line `3000164545`). Not ported — dead code.
|
||||
|
||||
WARNING — **the reference has live production credentials committed to git**
|
||||
(`Sms.php:74-76`: a panel username, a plaintext password, and a sender line).
|
||||
They are **not** copied into the target and are **not** repeated here. Logged as
|
||||
**SEC-12**. The owner should assume they are compromised and rotate them.
|
||||
|
||||
## 2. Message templates
|
||||
|
||||
`Sms::Send($to, $msg)` takes a fully-built string; there are no provider-side
|
||||
template ids. Lines are separated by a newline.
|
||||
|
||||
### OTP — the one this project needs
|
||||
|
||||
`ApiMobileController.php:47`:
|
||||
|
||||
```
|
||||
<#> code is {CODE}
|
||||
رمز عبور یکبار مصرف شما جهت ورود به اپلیکشن هنزا گلد {CODE} می باشد
|
||||
aMo6iFyUQaY
|
||||
```
|
||||
|
||||
The first line and the trailing `aMo6iFyUQaY` are the **Android SMS Retriever**
|
||||
hash — they must stay byte-identical or autofill breaks. This exact string is
|
||||
already in the target at `customer-auth.service.ts:219-222`, built and held
|
||||
ready, currently discarded via `void smsText`.
|
||||
|
||||
A second OTP variant exists for the web app (`WebApp/AuthController.php:21`),
|
||||
ending with the site domain and a hash. Not used by the B2B app.
|
||||
|
||||
### The other B2B messages (all in scope)
|
||||
|
||||
| # | Trigger | Text | Where |
|
||||
|---|---|---|---|
|
||||
| 1 | Customer created by admin | `هنزا گلد` / `همکار گرامی {fname} {lname}` / `نام کاربری : {mobile}` | `AdminController.php:380-383` |
|
||||
| 2 | Customer activated (`active == -1`) | `{name} عزیز ` / `حساب كاربري شما در اپليكيشن هنزا فعال شد.` / `هم اكنون مي توانيد محصولات جديد را بررسي و ثبت سفارش كنيد.` / `با مراجعه به لینک زیر می توانید وارد پروفایل کاربری خود شوید.` / `https://henzagold.com/customer/login` / `هنزا، راهكارهاي نوين تامين محصولات` | `AdminController.php:441-447` |
|
||||
| 3 | Financial document approved | `هنزا گلد` / `سند مالی شما توسط مدیر سیستم تایید شد.` | `AdminController.php:465`, `CashController.php:141` |
|
||||
| 4 | Financial document rejected | `هنزا گلد` / `سند مالی شما توسط مدیر سیستم رد شد.` | `CashController.php:144` |
|
||||
| 5 | Order approved | `هنزا گلد` / `سفارش شما با شماره سند {oId}تایید شد.` (no space before «تایید» in the reference) | `AdminController.php:522` |
|
||||
| 6 | Order placed — normal | `سفارش شما به شماره {id} به ثبت رسید. هنزا گلد` | `ApiMobileController.php:742` |
|
||||
| 7 | Order placed — over `gram_level` | `سفارش شما به شماره {id} به ثبت رسید اما به دلیل عبور از سقف مجاز گرمی در حالت معلق قرار گرفت. هنزا گلد` | `ApiMobileController.php:739` |
|
||||
| 8 | Order placed by the sales expert for a customer | `هنزا گلد` / `همکار گرامی {fname} {lname}` / `سفارشی به وزن {allGram} گرم توسط آقای {reagentName} برای شما در سیستم ثبت شده است.` / `در صورت مورد تایید بودن سفارش بر روی لینک زیر کلیک نمایید.` / `{confirmLink}` | `ApiMobileController.php:733` |
|
||||
| 9 | To the customer's sales expert, on every order | `هنزا گلد` / `سفارشی توسط {fname} {lname} به شماره {id}و به وزن {allGram} به ثبت رسید.` | `ApiMobileController.php:749` |
|
||||
| 10 | To four hardcoded admin numbers, on every order | `هنزا گلد` / `سفارشی توسط {fname} {lname}` / ` به وزن {round(allGram)}` / `در سیستم ثبت شد.` | `ApiMobileController.php:601-605` |
|
||||
| 11 | To three hardcoded admin numbers, on findoc submit | `هنزا گلد` / `سندی توسط آقای / خانم {fname} {lname} در سیستم ثبت شد.لطفا نسبت به بررسی و تایید آن اقدام نمایید. ` | `ApiMobileController.php:154-158` |
|
||||
| 12 | Admin free-text / batch | operator-supplied | `SmsController.php:56,151` |
|
||||
|
||||
Messages 10 and 11 go to **phone numbers hardcoded in the source**
|
||||
(six different mobile numbers across `ApiMobileController.php:154-158,601-605`).
|
||||
Porting those literals would be wrong — they belong in configuration. Proposal:
|
||||
one env var holding a comma-separated list, defaulting to empty. Logged as
|
||||
**STUB-05**.
|
||||
|
||||
**Storefront messages are excluded** per D13: the cashback/wallet texts
|
||||
(`AdminController.php:921,951`), `Customer/OrderController.php:94,141-145`,
|
||||
`SingleCustomerController.php:66`, `Site/SiteController.php:182`,
|
||||
`birthdayCommand.php`, `SmsWalletCommand.php`.
|
||||
|
||||
## 3. Proposed env vars — names only, no values in code or git
|
||||
|
||||
| Name | Required | Purpose |
|
||||
|---|---|---|
|
||||
| `SMS_PROVIDER` | no, default `trez` | selects the driver; `none` keeps today's dev behaviour |
|
||||
| `SMS_TREZ_URL` | no, defaults to the reference endpoint | provider endpoint |
|
||||
| `SMS_TREZ_USERNAME` | **yes in production** | panel username |
|
||||
| `SMS_TREZ_PASSWORD` | **yes in production** | panel password |
|
||||
| `SMS_TREZ_SENDER_LINE` | **yes in production** | the `PhoneNumber` form field |
|
||||
| `SMS_ADMIN_RECIPIENTS` | no, default empty | replaces the hardcoded admin numbers (STUB-05) |
|
||||
|
||||
Proposed behaviour, per the owner's instruction:
|
||||
|
||||
- **Key absent, `NODE_ENV !== 'production'`** — today's behaviour is unchanged:
|
||||
the code is logged once by `Logger`, a redacted audit row is written.
|
||||
- **Key absent, `NODE_ENV === 'production'`** — the server **fails to boot** with
|
||||
an explicit error naming the missing variable. No silent no-op, because a
|
||||
silent no-op means no customer can log in and nothing says why.
|
||||
- Secrets are read from `process.env` only; nothing is written to the repo, and
|
||||
`.env.example` carries the **names with empty values**.
|
||||
|
||||
**Awaiting approval before implementing.**
|
||||
|
||||
---
|
||||
|
||||
# Report — MON-08: where the pricing values come from (2026-09-22)
|
||||
|
||||
Requested: in `henza_pwa`, does the user type `wagePercent` / `discountPercent` /
|
||||
`flatRialCharge`, or does the app compute them?
|
||||
|
||||
## Answer: the app computes all of them. The user types none of them.
|
||||
|
||||
The whole order payload is built in one place,
|
||||
`henza_pwa/src/views/cart.vue:160-184` (`submitOrder`):
|
||||
|
||||
| Payload field | Target equivalent | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| `percent_pay` | `wagePercent` | `getWage(variant)` = `myStore.state.percent + variant.percentage_pay` | `cart.vue:173` -> `src/Mixins/product/index.ts:69-75` |
|
||||
| `discount` | `discountPercent` | `getPromotion(product)?.discount_percent ?? 0` | `cart.vue:170` |
|
||||
| `rial_pay` | `flatRialCharge` | **hardcoded `0`** — no UI writes it | `cart.vue:179` |
|
||||
| `rash_weight` | (target reads from DB) | `cart.variant(cartItem).grams` — the variant's own weight | `cart.vue:178` |
|
||||
| `qty` | `quantity` | the cart stepper — **the only genuine user input** | `cart.vue:177` |
|
||||
|
||||
Both computed inputs come from server-issued data:
|
||||
|
||||
- `variant.percentage_pay` — the variant's making charge, from the catalogue.
|
||||
- `myStore.state.percent` — **`customers.percent`**, delivered by `POST /me-data`
|
||||
and stored at `myDataStore.ts:29,59,122-127`. This is the same
|
||||
`makingChargeOffsetPercent` field of D9, and it **confirms that trace from the
|
||||
client side**: the offset is per-customer and it is added to the making charge.
|
||||
- `promotion.discount_percent` — from the promotion the server returned.
|
||||
|
||||
Two fields are sent as **literal constants with a `// how to get?!` comment**
|
||||
(`cart.vue:181-182`): `wage_weight: 5.7856000000000005` and
|
||||
`weight: 6.303744000000001`. The server ignores both — `setOrder` recomputes the
|
||||
weight from `rash_weight`, and the `wage_weight` use is commented out
|
||||
(`ApiMobileController.php:549-551`). Dead payload fields; logged as **DEAD-06**.
|
||||
|
||||
## What the server does with them
|
||||
|
||||
`ApiMobileController.php:536-541` computes the customer's **gold debt** directly
|
||||
from the numbers in the request:
|
||||
|
||||
```php
|
||||
$percent = ($item['percent_pay'] ?? 0) - ($item['discount'] ?? 0);
|
||||
$gram = ($item['rash_weight'] ?? 0) + (($item['rash_weight'] ?? 0) * ($percent / 100));
|
||||
return $gram * ($item['qty'] ?? 0);
|
||||
```
|
||||
|
||||
Nothing is re-read from the database and nothing is validated. A crafted request
|
||||
sets its own making charge, its own discount and its own weight — so the
|
||||
reference lets a customer decide how much gold they owe.
|
||||
|
||||
## Consequence — per the owner's ruling
|
||||
|
||||
The app computes these values, so the ruling applies:
|
||||
|
||||
> «اگر اپ حساب میکند: اولین کار Phase 2 این باشد که سرور همان مقادیر را با همان
|
||||
> منطق از DB بخواند و مقادیر کلاینت را نادیده بگیرد، با تست مقایسهای که نشان
|
||||
> دهد نتیجه برای کاربر عادی یکسان است.»
|
||||
|
||||
**Phase 2, item #1** (recorded in `PROGRESS.md`):
|
||||
|
||||
1. The server derives, per line:
|
||||
`wagePercent = customer.makingChargeOffsetPercent + variant.percentagePay`,
|
||||
`discountPercent` from the applicable promotion, `flatRialCharge = 0`.
|
||||
2. Client-sent values are **ignored**, and a warning is logged when they differ —
|
||||
the same pattern already used for `customerId` in `orders.service.ts`.
|
||||
3. **A comparison test** proves that for an ordinary customer the server-derived
|
||||
values equal what the app sends today, so the total is unchanged.
|
||||
4. The formula, its ordering and its rounding are untouched (D4).
|
||||
|
||||
This depends on `makingChargeOffsetPercent` existing, so the percent migration
|
||||
must land first.
|
||||
|
||||
## `rash_weight` — accepted deviation
|
||||
|
||||
The reference trusts the client's `rash_weight` (`ApiMobileController.php:538`);
|
||||
the target already reads the weight from the variant in the database. **Per the
|
||||
owner's ruling the target's behaviour stays.** Recorded here as a deliberate
|
||||
security deviation from the reference. The visible result is identical whenever
|
||||
the client sends the true variant weight, which the reference app always does
|
||||
(`cart.vue:178`); it differs only for a forged request, which is the point.
|
||||
|
||||
@@ -104,3 +104,11 @@ Severity: `Critical` (money loss / security / data loss) · `High` · `Medium`
|
||||
|---|---|---|---|---|---|
|
||||
| DOC-01 | Medium | DOC `docs/parity/_inv-ref-server.md:588` (§3.6) | The Phase 0 inventory undercounted `routes/admin.php`: it recorded **117** explicit routes where the file has **151** live ones (6 more are commented out). Every downstream total inherited the error, so the reference endpoint count was reported as **369** when it is **412** (admin 249, not 215). No code is affected — only the planning figures. | `grep -E "^\s*Route::(get\|post\|put\|patch\|delete\|any)" routes/admin.php \| grep -v "^\s*//" \| wc -l` → 151; resources 14×7=98; 151+98=249 | ✅ Corrected in `00-INVENTORY.md` §2.1 and `DECISIONS.md` §D13. `_inv-ref-server.md` left as the raw subagent record. |
|
||||
| DOC-02 | Low | REF `routes/admin.php:38` | `GET admin/get-analytics-tak` reads `SingleCustomerOrders` (`Adm/AdminController.php:557-579`), so it is a **retail-storefront** endpoint despite its neutral name. A keyword scan for `wallet\|single\|comment` misses it. Noted so the D13 scope split stays correct — anything else named neutrally but querying `SingleCustomerOrders`, `Fav`, `Address`, `Comment` or `Transaction` is storefront too. | `AdminController.php:563` — `SingleCustomerOrders::select(...)` | ✅ Classified as ⏸️ storefront in D13 (the 31st admin storefront route). |
|
||||
|
||||
## Found during the SMS / MON-08 investigation (2026-09-22)
|
||||
|
||||
| ID | Severity | Where | Description | Evidence | Status |
|
||||
|---|---|---|---|---|---|
|
||||
| SEC-12 | **Critical** | REF `henza_server/app/Models/Sms.php:73-77,101-105` | Live SMS-panel production credentials (username, plaintext password, sender line) are **committed to git** in the reference, duplicated across two methods. An older provider's credentials sit commented out beside them. Anyone with repo access can send SMS on the owner's account at their cost. | Read the file. | Not ported — the target will read them from env only. **The owner should rotate these credentials**; they must be assumed compromised. |
|
||||
| STUB-05 | Medium | REF `ApiMobileController.php:154-158,601-605`; `Adm/OrderController.php:54-55`; `Customer/OrderController.php:141-145` | Admin notification recipients are **hardcoded mobile numbers** scattered across controllers (six distinct numbers). Staff changes require a code deploy, and an ex-employee keeps receiving order data. | Read the files. | Port as `SMS_ADMIN_RECIPIENTS` (comma-separated, default empty) rather than copying the literals. Pending the SMS approval. |
|
||||
| DEAD-06 | Low | REF `henza_pwa/src/views/cart.vue:181-182` | The cart sends `wage_weight: 5.7856000000000005` and `weight: 6.303744000000001` as **literal constants**, each with a `// how to get?!` comment. The server ignores both: it recomputes weight from `rash_weight`, and the `wage_weight` use is commented out (`ApiMobileController.php:549-551`). | Read both files. | Dead payload fields — do not port. |
|
||||
|
||||
+28
-2
@@ -10,8 +10,8 @@
|
||||
| **Phase** | Security hotfix — **stages 1 and 2 ✅ complete** |
|
||||
| **Branch** | `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** | Show the user the **D0 (firstName/lastName)** and **`percent`** migrations for approval, then start `01-ADMIN-MATRIX.md` field-by-field (D12) |
|
||||
| **Blocked on** | user approval of those two migrations before they run |
|
||||
| **Next step** | **Migrations are written and shown — awaiting approval to run them** (`migrate dev`), then `01-ADMIN-MATRIX.md` field-by-field (D12) |
|
||||
| **Blocked on** | approval to run the two migrations; approval to implement the SMS provider |
|
||||
| **Last updated** | 2026-09-22 |
|
||||
|
||||
### Security hotfix — final result
|
||||
@@ -146,3 +146,29 @@ All user rulings live there (D0–D13). Highlights that change how work is done:
|
||||
| `_inv-tgt-userappv2.md` | apps/user-app-v2 | 700 |
|
||||
|
||||
Consolidated into `00-INVENTORY.md`. Issues found: `ISSUES.md` (26 entries).
|
||||
|
||||
## Standing rules added 2026-09-22
|
||||
|
||||
1. **Migrations run only via the standard Prisma workflow** (`migrate dev` in
|
||||
development). **`prisma db execute` is not used for schema changes.**
|
||||
2. **Every data deletion is logged in `CHANGELOG.md`** — including throwaway
|
||||
rows created during a session.
|
||||
3. **D13** — the retail storefront is preserved but out of scope; see
|
||||
`DECISIONS.md` §D13 and `PROTECTED.md` §H.
|
||||
|
||||
## Phase 2 — item #1 (fixed by the MON-08 ruling)
|
||||
|
||||
Before anything else in Phase 2: the server must derive `wagePercent`,
|
||||
`discountPercent` and `flatRialCharge` from the DB and **ignore** the client's
|
||||
values, because `henza_pwa` computes them rather than asking the user
|
||||
(`cart.vue:170-179`, `Mixins/product/index.ts:69-75`). A comparison test must
|
||||
show the total is unchanged for an ordinary customer. Depends on
|
||||
`makingChargeOffsetPercent` existing. Full reasoning: `DECISIONS.md`
|
||||
§"Report — MON-08".
|
||||
|
||||
## Waiting on the owner (2026-09-22)
|
||||
|
||||
| # | Item | What is ready |
|
||||
|---|---|---|
|
||||
| 1 | Run the **D0** and **percent** migrations | Both SQL files written, storefront impact assessed, backfill report script written, 11 unit tests passing. Command: `npx prisma migrate dev`. |
|
||||
| 2 | Implement the **SMS provider** (Trez) | Provider, all 12 templates and env-var names documented in `DECISIONS.md`. Nothing written yet. **SEC-12: the reference's committed credentials must be rotated.** |
|
||||
|
||||
Reference in New Issue
Block a user