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>
128 lines
5.2 KiB
Markdown
128 lines
5.2 KiB
Markdown
# 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.
|