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>
5.2 KiB
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.
- Leave
20260917120000_...untouched. Editing a migration Prisma has recorded would make its checksum mismatch and breakmigrate statusfor everyone. Its only sin is a misleading comment, and a follow-up migration makes that harmless. - Write the D0 and
percentwork 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. - 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. - Never
migrate reset, and never edit_prisma_migrationsby 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.