Files
s.esmaeiliandClaude Opus 5 024e3dd045 parity(api,apps): close SEC-02 - orders take the customer from the token
The user declined to defer this to Phase 2, correctly: a public POST /orders
with customerId in the body let anyone place an order on any customer's
account, and an order lands on that customer's gold debt and ledger.

POST /orders is now behind CustomerAuth. OrdersService.createOrder takes the
customer id as a separate argument that the controller fills from the token;
the body's customerId is ignored and a mismatch is logged. The field stays
optional in the DTO and in shared-types only so both apps keep validating
while they still send it -- they drop it in Phase 2. The admin builder is
unaffected: it legitimately places orders for a chosen customer and passes
that id explicitly, behind the admin guard.

GET /orders/:id is scoped to the owner. A foreign or nonexistent id both
return 404, never 403, so ids stay unenumerable. Operators read through the
new GET /orders/admin/:id behind rid order.list, declared before the :id
route because Nest matches in declaration order.

Both apps' CartView now gates on session.isLoggedIn rather than a remembered
customerId, which is no longer a credential.

OTP audit (requested): no master code, no bypass, no hardcoded code anywhere;
one code path issues a token and only after bcrypt.compare. No SMS provider is
wired up -- in development the code is logged once, guarded by
NODE_ENV !== 'production'.

Fixed while auditing (SEC-11): deliver() persisted the real message text, code
included, into SmsMessage.body, which the admin SMS grid lists and full-text
searches. Any operator with the sms rid could read live codes, defeating the
hashing of codeHash. The stored row is now a redacted audit line.

Also logged MON-08: wagePercent/discountPercent/flatRialCharge are still taken
from the client on order creation. The reference is worse -- it takes
rash_weight from the client too -- so this is already a partial improvement;
logged rather than changed, per D4.

Tests: auth-smoke.sh now 53 passed, 0 failed, including a spoofed body
customerId being ignored in favour of the token. Typecheck clean on api,
user-app, user-app-v2 and shared-types.

[ref: henza_server ApiMobileController.php:533,536-545,615-632]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 07:09:33 +03:30

128 lines
5.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Migration state — investigated before writing D0 / percent
Requested by the user before any new migration is written. All findings are from
**read-only** queries; nothing was reset and `_prisma_migrations` was not touched.
## The five questions
### 1. Is `20260917120000_customer_name_parts_and_percent` recorded as applied?
**Yes — but as a no-op.**
```
migration_name 20260917120000_customer_name_parts_and_percent
finished_at 2026-09-18T16:03:33.956Z
rolled_back_at null
applied_steps_count 0 <-- zero steps ran
```
`finished_at` is set and `rolled_back_at` is null, so **Prisma considers it
applied and will never re-run it**. `applied_steps_count = 0` says no SQL
statement was ever executed by it.
### 2. What is in its SQL file?
**Comments only — no executable statement.** The file is a placeholder that
explains itself: it says the columns were added "directly against the live DB"
and that it was recreated as an empty no-op so `prisma migrate status` would
stop erroring on a missing directory.
### 3. Does `schema.prisma` declare `firstName` / `lastName` / `productPercent`?
**No.** `grep` over `prisma/schema.prisma` finds none of the three. The
`Customer` model has `fullName` and nothing else name-related.
### 4. Do the columns exist in the database?
**No.** Querying `information_schema.columns` for `Customer` and those names
returns only `fullName`:
```
firstName absent
lastName absent
productPercent absent
makingChargeOffsetPercent absent
fullName present
```
**So the placeholder file's own comment is wrong.** It claims the columns "already
exist live"; they do not, at least not in this database (`gold_banking` on
localhost). Whatever database that comment described, it was not this one.
**Consequence for the runtime:** none, and this is the good news. Prisma Client
generates from `schema.prisma`, and the schema does not mention these fields
either. Schema and database agree — both lack the columns. There is **no drift
that could make Prisma Client throw at runtime**. The only thing that is wrong is
the placeholder's comment.
### 5. `prisma migrate status` (read-only)
```
9 migrations found in prisma/migrations
Following migration have not yet been applied:
20260922000000_customer_otp
```
Note on that line: the OTP table **does exist** — stage 2 created it with
`prisma db execute`, which runs SQL without writing a ledger row. So the schema
is correct but the ledger entry is missing, and `migrate deploy` would try to
run it again. The SQL is written with `CREATE TABLE IF NOT EXISTS`, the indexes
with `IF NOT EXISTS`, and the FK inside a `DO $$ ... EXCEPTION WHEN
duplicate_object` block, so a re-run is harmless. Still worth resolving cleanly —
see the recommendation.
### 6. Where did the file come from?
```
b80fa9b Redesign product detail page and fix icon/cart reactivity bugs
```
A single commit, and the message has nothing to do with migrations — the
placeholder was swept in as collateral to an unrelated frontend change.
---
## Recommendation
**The placeholder is recorded as applied, so per the user's rule it must not be
edited.** It stays exactly as it is.
1. **Leave `20260917120000_...` untouched.** Editing a migration Prisma has
recorded would make its checksum mismatch and break `migrate status` for
everyone. Its only sin is a misleading comment, and a follow-up migration
makes that harmless.
2. **Write the D0 and `percent` work as a new, additive migration** with a fresh
timestamp. It adds the columns the placeholder never added. Written to be
idempotent (`IF NOT EXISTS`) so it is safe on a database where someone *did*
apply the columns by hand.
3. **Resolve the OTP ledger entry** with
`npx prisma migrate resolve --applied 20260922000000_customer_otp`.
That writes the ledger row without running SQL, which is exactly right here
since the table already exists. **Needs the user's approval before running**,
as it writes to `_prisma_migrations`.
4. **Never** `migrate reset`, and never edit `_prisma_migrations` by hand.
---
## Naming: one field, not two
The user asked for a single final name. **`productPercent` is dropped;
`makingChargeOffsetPercent` is the name.**
Reason: the reference has **three unrelated things** all called `percent`, and a
vague name is how they got confused in the first place (the target currently
reads the wrong one — CUST-02):
| Reference `percent` | Where | What it is |
|---|---|---|
| `customers.percent` | `customer_add.php:32` | a **per-customer making-charge offset**, used only to shift catalogue filter bounds (`ApiMobileController.php:391-396,76-77`) |
| `ConfigStatic['percent']` | `AdminController.php:903,994` | the **wallet cashback rate**: `total_weight × percent/100` |
| `order_item.percent` | `MajorOrderController.php:220` | the **per-line making charge** on a wholesale order |
`productPercent` would read as "a percent belonging to a product", which is the
third meaning, not the first. `makingChargeOffsetPercent` says which of the
three it is. It is mapped back to the legacy key `percent` in `toRow()`, so the
admin panel's field name does not change.
`productPercent` is not created, so there is no duplicate field.