docs: data migration map — precheck results, owner rulings, run status
DATA-MIGRATION-MAP.md now records: - the precheck numbers from the live henza DB and the assumptions they overturned: verify steps are 1/2/3, not boolean; reagent_id points at customers, not users; cashes.remain/rial_remain are NULL on every row - owner rulings: mobile as the match key, sales expert wins conflicts, merge rather than import, allow-list columns only, retail orders_orders deferred, fav and birth_date kept, token_mobile and marriage_date dropped - run status of the prep migration and the REJECTED consumer audit - a process hazard: on the dev machine, a file in prisma/migrations is applied by the watch-mode server on the next src/ edit - two open owner questions about the live verify() flow migration-precheck-3.sql: read-only (COUNT only). Checks where new_city should land and whether birth_date holds Jalali years. PROGRESS.md: workstream row pointing here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
884413513d
commit
4d3b15c3b9
+421
-22
@@ -307,11 +307,33 @@ Prisma فقط `remain` را بهعنوان ستون دارد. در مدل ج
|
|||||||
محاسبهشان کند** — وگرنه عدد صفر نشان داده میشود.
|
محاسبهشان کند** — وگرنه عدد صفر نشان داده میشود.
|
||||||
|
|
||||||
**ج) دادهی واقعیِ غیرقابلبازسازی** — `birth_date`, `marriage_date`,
|
**ج) دادهی واقعیِ غیرقابلبازسازی** — `birth_date`, `marriage_date`,
|
||||||
`token_mobile`, `new_city`. **اینها اگر منتقل نشوند، برای همیشه از بین
|
`token_mobile`, `new_city`.
|
||||||
میروند.** `token_mobile` در ۱۰ فایل استفاده میشود؛ بدون آن، اعلان push به
|
|
||||||
گوشیهای موجود قطع میشود.
|
|
||||||
|
|
||||||
🔶 **تصمیم مالک لازم است برای گروه (ج).**
|
### ✅ تصمیم مالک ۲۰۲۶-۱۰-۰۷ — دو منتقل، دو حذف
|
||||||
|
|
||||||
|
| ستون | ردیف | تصمیم | دلیل مالک |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `birth_date` | **۹۱۷** | ✅ **منتقل** | «نرخ پرشدگی بالای ۵۵٪ · کمپینهای وفاداری و پیامکی» |
|
||||||
|
| `new_city` | **۱۷۶** | ✅ **منتقل** | «خالی ماندن شهر ۱۷۶ مشتری نقص دیتا ایجاد میکند» |
|
||||||
|
| `token_mobile` | ۹۹ | ❌ **حذف** | «توکنها اغلب منقضی شدهاند · زباله دیتایی» |
|
||||||
|
| `marriage_date` | ۵ | ❌ **حذف** | «۵ ردیف توجیه معماری ندارد» |
|
||||||
|
|
||||||
|
**هر دو حذف قابلدفاعاند.** توکن push با تغییر کلید FCM/APNs بیاثر
|
||||||
|
میشود، و ۵ ردیف ارزش یک ستون دائمی را ندارد.
|
||||||
|
|
||||||
|
migration افزایشی لازم:
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model Customer {
|
||||||
|
birthDate DateTime? // legacy birth_date
|
||||||
|
// new_city → §۵.۹
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔶 `new_city` — «کدام فیلد» هنوز باز است
|
||||||
|
|
||||||
|
مالک گفت «یا در فیلد شهر بنشیند یا در یک فیلد آدرس متنی مپ شود». کدامیک،
|
||||||
|
به داده بستگی دارد — §۵.۹.
|
||||||
|
|
||||||
`api_token`, `send_password`, `rnd` نیازی نیست منتقل شوند — سامانهی auth
|
`api_token`, `send_password`, `rnd` نیازی نیست منتقل شوند — سامانهی auth
|
||||||
جدید JWT است.
|
جدید JWT است.
|
||||||
@@ -388,14 +410,141 @@ Prisma فقط `remain` را بهعنوان ستون دارد. در مدل ج
|
|||||||
جایی برای رفتن ندارند و اگر `UNVERIFIED` شوند، **از «ردشده» به
|
جایی برای رفتن ندارند و اگر `UNVERIFIED` شوند، **از «ردشده» به
|
||||||
«بررسینشده» برمیگردند** — یعنی دوباره در صف بررسی ظاهر میشوند.
|
«بررسینشده» برمیگردند** — یعنی دوباره در صف بررسی ظاهر میشوند.
|
||||||
|
|
||||||
🔶 **تصمیم مالک — دو گزینه:**
|
### ✅ تصمیم مالک ۲۰۲۶-۱۰-۰۷ — `REJECTED` + علت رد
|
||||||
1. افزودن `REJECTED` به enum (additive، با §G تضاد ندارد) — ۱۴ ردیف درست
|
|
||||||
منتقل میشوند
|
|
||||||
2. نگاشت ردشدهها به `UNVERIFIED` و پذیرفتن اینکه در صف برمیگردند
|
|
||||||
|
|
||||||
و **۱۰ ردیف ناسازگار** (`1,2` و `3,2`) هر تصمیمی بگیرید، نیاز به قاعدهی
|
> «رویکرد شما ناقص است؛ حالت «رد شد» باید همراه با فیلد «علت رد» اضافه شود.
|
||||||
صریح دارند: گام دوم تأیید شده ولی گام اول نه. در legacy این حالت ممکن بود
|
> انتقال این افراد به حالت «در انتظار» کاملاً مردود است چون بار عملیاتی و
|
||||||
چون دو ستون مستقلاند و هیچ محدودیتی بینشان نیست.
|
> خطای انسانی ایجاد میکند.»
|
||||||
|
|
||||||
|
**پذیرفته شد.** انتقال به `UNVERIFIED` رد شد.
|
||||||
|
|
||||||
|
**دو تغییر افزایشی روی مقصد:**
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
enum VerificationStep {
|
||||||
|
UNVERIFIED
|
||||||
|
STEP_ONE_SUBMITTED
|
||||||
|
STEP_TWO_SUBMITTED
|
||||||
|
VERIFIED
|
||||||
|
REJECTED // ← افزوده
|
||||||
|
}
|
||||||
|
|
||||||
|
model Customer {
|
||||||
|
// ...
|
||||||
|
verificationRejectionReason String? // ← افزوده
|
||||||
|
verificationRejectedAt DateTime? // ← افزوده
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔴 ولی legacy علت رد را ذخیره نمیکند
|
||||||
|
|
||||||
|
مالک خواست «در صورت وجود در سیستم قدیم» علت هم منتقل شود. **بررسی شد:
|
||||||
|
وجود ندارد.**
|
||||||
|
|
||||||
|
هر ۴۹ ستون `customers` خوانده شد — **هیچ ستون `reason`, `note`,
|
||||||
|
`description`, `comment` یا مشابهی ندارد.** جستوجوی کل دامپ هم نشان داد
|
||||||
|
`canceled_reason` فقط روی `orders_orders` است و `reason` روی یک جدول
|
||||||
|
`transactions_*`.
|
||||||
|
|
||||||
|
و در `MajorCustomerController.php:129-134`، هنگام رد، legacy **فقط یک
|
||||||
|
پیامک ثابت** میفرستد:
|
||||||
|
|
||||||
|
```
|
||||||
|
'اطلاعات شما تایید نشده است.'
|
||||||
|
'لطفا با پیشتیبانی تماس حاصل فرمایید، تا مشکل برطرف شده و ثبتنام شما تکمیل شود.'
|
||||||
|
```
|
||||||
|
|
||||||
|
متن ثابت است — **هیچ علتی در آن نیست و هیچجا ذخیره نمیشود.** یعنی در
|
||||||
|
سیستم قدیم هم پشتیبان علت را نمیدانست؛ مشتری باید تلفن میزد.
|
||||||
|
|
||||||
|
### پس نتیجهی عملی
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| ستون `verificationRejectionReason` | ✅ **ساخته میشود** (خواست مالک · برای آینده) |
|
||||||
|
| مقدارش در مهاجرت ۱۴ ردیف | **`NULL`** — چون مبدأ دادهای ندارد |
|
||||||
|
| `verificationRejectedAt` | **`NULL`** — legacy زمان رد را هم ثبت نمیکرد |
|
||||||
|
|
||||||
|
⚠️ **این یک شکاف دادهی ذاتی است، نه نقص ابزار مهاجرت.** علت رد آن ۱۴
|
||||||
|
مشتری در سیستم قدیم هرگز ذخیره نشده بود و **قابل بازیابی نیست**. ستون
|
||||||
|
ساخته میشود تا از این پس پر شود؛ برای این ۱۴ نفر خالی میماند.
|
||||||
|
|
||||||
|
### ✅ تصمیم مالک ۲۰۲۶-۱۰-۰۷ — یادداشت خودکار درج شود
|
||||||
|
|
||||||
|
> «فیلد علت برای این ۱۴ ردیف نباید کامپلت NULL رها شود.»
|
||||||
|
|
||||||
|
مقدار دقیق که ابزار درج میکند:
|
||||||
|
|
||||||
|
```
|
||||||
|
رد شده در سیستم قدیم — فاقد شرح علت ثبتشده
|
||||||
|
```
|
||||||
|
|
||||||
|
| ستون | مقدار |
|
||||||
|
|---|---|
|
||||||
|
| `verificationStep` | `REJECTED` |
|
||||||
|
| `verificationRejectionReason` | **متن بالا** (نه `NULL`) |
|
||||||
|
| `verificationRejectedAt` | `NULL` — legacy زمان رد را ثبت نمیکرد |
|
||||||
|
|
||||||
|
⚠️ **قاعدهی ابزار:** این متن **فقط** روی ردیفهایی درج شود که مهاجرت
|
||||||
|
`REJECTED`شان میکند. اگر ابزار دوباره اجرا شود، نباید روی مشتریای که
|
||||||
|
**در سیستم جدید** رد شده و علت واقعی دارد بازنویسی کند. شرط:
|
||||||
|
`WHERE verificationRejectionReason IS NULL` — یا طبق §۱۰.۵، این ستون
|
||||||
|
هم در allow-list فقط برای ردیفهای تازه نوشته شود.
|
||||||
|
|
||||||
|
### ۱۰ ردیف ناسازگار
|
||||||
|
|
||||||
|
`1,2` (۷ ردیف) و `3,2` (۳ ردیف) — گام دوم تأیید شده ولی گام اول نه.
|
||||||
|
در legacy ممکن بود چون دو ستون مستقلاند و هیچ محدودیتی بینشان نیست.
|
||||||
|
|
||||||
|
**قاعدهی پیشنهادی: گام اول حاکم است.**
|
||||||
|
- `1,2` → `STEP_ONE_SUBMITTED` (گام اول هنوز بررسی نشده)
|
||||||
|
- `3,2` → `REJECTED` (گام اول رد شده، پس کل پرونده رد است)
|
||||||
|
|
||||||
|
دلیل: در legacy `step_one=3` ستون `active` را `-1` میکرد، یعنی مشتری
|
||||||
|
**غیرفعال** میشد. پس رد گام اول واقعاً پرونده را میبست.
|
||||||
|
|
||||||
|
### ۵.۹ — `new_city` چیست و کجا بنشیند
|
||||||
|
|
||||||
|
`new_city` یک **نام شهر بهصورت متن آزاد** است، مستقل از `city_id`.
|
||||||
|
`AdminController.php:296-298` و `Major/CustomerController.php:36` نشان
|
||||||
|
میدهند هرگاه فرم فیلد `city` را پر بفرستد، عیناً در `new_city` ذخیره
|
||||||
|
میشود — **بدون هیچ تطبیقی با جدول `cities`**.
|
||||||
|
|
||||||
|
و `MajorCustomerController.php:36` ثابت میکند **خوانده میشود**:
|
||||||
|
داشبورد مشتری `new_city` را در `select` دارد. پس ستون زنده است، نه
|
||||||
|
بازمانده.
|
||||||
|
|
||||||
|
**چرا ۱۷۶ ردیف دارد:** جدول `cities` فقط **۱۵ ردیف** دارد (§۱۰.۴). پس
|
||||||
|
بسیاری از شهرهای واقعی در آن نیستند و اپراتور نام را دستی تایپ میکرده.
|
||||||
|
|
||||||
|
### دو حالت ممکن، دو راهحل متفاوت
|
||||||
|
|
||||||
|
| اگر | یعنی | راهحل |
|
||||||
|
|---|---|---|
|
||||||
|
| اکثر ۱۷۶ ردیف `city_id` **تهی** دارند | شهرشان در `cities` نیست | **ستون متنی لازم است** |
|
||||||
|
| اکثرشان `city_id` **هم** دارند | `new_city` تکراری است | میتوان رهایش کرد |
|
||||||
|
| متنشان با عنوان یک شهر موجود یکی است | قابل تبدیل به `city_id` | نگاشت به `cityId` |
|
||||||
|
|
||||||
|
🔶 **یک کوئری لازم است** —
|
||||||
|
[`migration-precheck-3.sql`](./migration-precheck-3.sql). تا جوابش نیاید،
|
||||||
|
نمیدانم ستون تازه بسازم یا به `cityId` نگاشت کنم. ساختن ستونی که لازم
|
||||||
|
نیست، همانقدر بد است که نساختن ستونی که لازم است.
|
||||||
|
|
||||||
|
⚠️ **اگر ستون متنی لازم شد، اسمش `cityNameRaw` پیشنهاد میشود**، نه
|
||||||
|
`newCity` — چون «new» در legacy معنای تاریخی داشت (ستون بعداً اضافه شده)
|
||||||
|
و در مقصد گمراهکننده است.
|
||||||
|
|
||||||
|
### `birth_date` — یک بررسی سلامت
|
||||||
|
|
||||||
|
`birth_date` نوعش `timestamp` است. ولی چون legacy تاریخها را جاهای دیگر
|
||||||
|
**بهصورت رشتهی جلالی** ذخیره میکرد (§۸ — `recive_date`)، احتمال دارد
|
||||||
|
اینجا هم تاریخ جلالی بهاشتباه بهعنوان میلادی وارد شده باشد.
|
||||||
|
|
||||||
|
اگر سالها در بازهی ۱۳۰۰–۱۴۰۰ باشند، یعنی **جلالی در ستون میلادی** و
|
||||||
|
تبدیل لازم است. اگر ۱۹۳۰–۲۰۱۰ باشند، سالماند.
|
||||||
|
کوئریاش در همان فایل سوم است.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### ۵.۷ — 🔴 `reagent_id` به `customers` اشاره میکند، نه `users`
|
### ۵.۷ — 🔴 `reagent_id` به `customers` اشاره میکند، نه `users`
|
||||||
|
|
||||||
@@ -891,8 +1040,23 @@ nationalCode String @unique // ← NOT NULL
|
|||||||
🔴 **مهاجرت روی اولین ردیف میشکند.** و `@unique` روی رشتهی خالی یعنی
|
🔴 **مهاجرت روی اولین ردیف میشکند.** و `@unique` روی رشتهی خالی یعنی
|
||||||
فقط **یک** ردیف میتواند `''` باشد.
|
فقط **یک** ردیف میتواند `''` باشد.
|
||||||
|
|
||||||
🔶 **تصمیم فوری لازم:** `nationalCode` باید `String?` شود (nullable). این
|
### ✅ تصمیم مالک ۲۰۲۶-۱۰-۰۷ — nullable شود
|
||||||
یک migration افزایشی روی مقصد است. بدون آن مهاجرت مشتری **غیرممکن** است.
|
|
||||||
|
> «وقتی دیتاست مبدا فاقد این داده است، هیچ گزینه دیگری جز Nullable کردن
|
||||||
|
> فیلد وجود ندارد. ساختن کد ملی فیک تخلف دیتایی است و مانع از
|
||||||
|
> احراز هویتهای قانونی بعدی میشود.»
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
nationalCode String? @unique // بود: String @unique
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **نکتهی فنی که به نفع این تصمیم است:** در Postgres مقدار `NULL` در
|
||||||
|
ایندکس `@unique` **تکراری حساب نمیشود**. پس هزار ردیف با
|
||||||
|
`nationalCode = NULL` بیمشکل کنار هم مینشینند — ولی اگر بهجای `NULL`
|
||||||
|
رشتهی خالی `''` بگذاریم، **فقط یکی جا میشود**.
|
||||||
|
|
||||||
|
🔴 **قاعدهی اجباری ابزار: `''` را به `NULL` تبدیل کند، نه اینکه خالی
|
||||||
|
بفرستد.**
|
||||||
|
|
||||||
**۳. گِردکردن رخ میدهد — ۲۴ + ۳۲ ردیف.**
|
**۳. گِردکردن رخ میدهد — ۲۴ + ۳۲ ردیف.**
|
||||||
`gold_needs_rounding = 24` و `order_weight_needs_rounding = 32`. یعنی §۴
|
`gold_needs_rounding = 24` و `order_weight_needs_rounding = 32`. یعنی §۴
|
||||||
@@ -942,12 +1106,116 @@ pw_bcrypt 5 pw_other 0 pw_empty 1590
|
|||||||
```prisma
|
```prisma
|
||||||
passwordHash String // ← NOT NULL
|
passwordHash String // ← NOT NULL
|
||||||
```
|
```
|
||||||
🔴 **مهاجرت میشکند.** دو راه: `passwordHash` nullable شود، یا مهاجرت یک
|
### ✅ تصمیم مالک ۲۰۲۶-۱۰-۰۷ — hash غیرقابلورود، nullable نشود
|
||||||
hash تصادفیِ غیرقابلاستفاده بگذارد. **گزینهی دوم امنتر است** — ستون
|
|
||||||
اجباری میماند و هیچکس نمیتواند با رمز خالی وارد شود.
|
> «برداشتن اجبار رمز (Nullable کردن پسورد) یک ضدالگوی امنیتی است و احتمال
|
||||||
|
> بایپس شدن احراز هویت در باگهای آینده را بالا میبرد. نباید صرفاً یک
|
||||||
|
> رشته تصادفی پلین یا هش ضعیف بگذارید.»
|
||||||
|
|
||||||
|
**`passwordHash` اجباری میماند.** مقدارش برای ۱٬۵۹۰ ردیف یک hash
|
||||||
|
**عامدانه غیرقابلورود** میشود.
|
||||||
|
|
||||||
|
🔴 **تفکیک مهمی که مالک درست روی آن تأکید کرد:** «رمز تصادفی» و «hash
|
||||||
|
غیرقابلورود» یکی نیستند.
|
||||||
|
|
||||||
|
| روش | نتیجه |
|
||||||
|
|---|---|
|
||||||
|
| ❌ رشتهی تصادفیِ خام در ستون | `bcrypt.compare()` همیشه `false` میدهد — **ولی** قالبش نامعتبر است و بعضی کتابخانهها در این حالت throw میکنند یا رفتار نامعین دارند |
|
||||||
|
| ❌ hash یک رمز تصادفی | یک رمز **واقعی** وجود دارد که کار میکند؛ اگر جایی لاگ شده باشد، در دسترس است |
|
||||||
|
| ✅ **قالب نامعتبرِ عامدانه** | هیچ رمزی به آن نمیخورد، **و** قالبش صریحاً «غیرفعال» را اعلام میکند |
|
||||||
|
|
||||||
|
**روش پیشنهادی — پیشوند `!` که در Django و Rails قرارداد است:**
|
||||||
|
|
||||||
|
```
|
||||||
|
!OTP_ONLY_NO_PASSWORD_LOGIN
|
||||||
|
```
|
||||||
|
|
||||||
|
چرا کار میکند: `bcrypt.compare(anyPassword, "!OTP_ONLY...")` هرگز `true`
|
||||||
|
نمیدهد چون رشته با `$2a$`/`$2b$`/`$2y$` شروع نمیشود، پس **هیچ رمزی
|
||||||
|
نمیتواند با آن مطابقت کند**. و برخلاف رشتهی تصادفی، **خواندنی است** —
|
||||||
|
هر کسی که ستون را ببیند میفهمد این کاربر فقط OTP دارد.
|
||||||
|
|
||||||
|
### ⚠️ بررسی کد مقصد — نگرانی باریکتر از آن است که بهنظر میرسد
|
||||||
|
|
||||||
|
مالک نگران «بایپس شدن احراز هویت در باگهای آینده» بود. کد مقصد خوانده
|
||||||
|
شد و دو واقعیت پیدا شد:
|
||||||
|
|
||||||
|
**۱. مشتری اصلاً مسیر ورود با رمز ندارد.**
|
||||||
|
`grep` روی کل `apps/api/src` نشان داد تنها دو جای `bcrypt.compare` روی
|
||||||
|
رمز اینهاست: `auth.service.ts:103` و `:135` — و هر دو داخل
|
||||||
|
`AuthService.login()` هستند که `prisma.adminUser.findUnique()` میخواند
|
||||||
|
(`auth.service.ts:40`)، **نه `customer`**.
|
||||||
|
|
||||||
|
ورود مشتری در `customer-auth.service.ts` است و تنها `bcrypt.compare`
|
||||||
|
آن روی `challenge.codeHash` است (خط ۳۶۱) — **کد OTP، نه رمز**.
|
||||||
|
`Customer.passwordHash` در هیچ مسیر احراز هویتی خوانده نمیشود.
|
||||||
|
|
||||||
|
**۲. مقصد از قبل `passwordHash: ''` مینویسد.**
|
||||||
|
در **پنج جا**: `customer-auth.service.ts:261`,
|
||||||
|
`admin-customers.service.ts:626,690`, `accounting-sync.service.ts:211`,
|
||||||
|
`admin-branches.service.ts:330`.
|
||||||
|
|
||||||
|
یعنی مشتریهایی که **همین حالا** از طریق OTP یا پنل ادمین ساخته میشوند،
|
||||||
|
رشتهی خالی دارند. **این وضع موجود است، نه چیزی که مهاجرت معرفی کند.**
|
||||||
|
|
||||||
|
### پس نتیجه
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| مقدار پیشنهادی مهاجرت | `!OTP_ONLY_NO_PASSWORD_LOGIN` |
|
||||||
|
| ریسک بایپس | **صفر** — مشتری مسیر ورود با رمز ندارد |
|
||||||
|
| چرا با این حال انجام شود | **دفاع در عمق** (خواست مالک) + خوانا بودن |
|
||||||
|
|
||||||
|
✅ **و یک سود جانبی:** چون مقصد الان `''` مینویسد، این روش **بهتر** از
|
||||||
|
وضع موجود است. پیشنهاد میکنم آن پنج جا هم به همین مقدار تغییر کنند تا
|
||||||
|
یکدست شود — ولی **این تغییر کد است، نه مهاجرت**، و اجازهی جداگانه
|
||||||
|
میخواهد.
|
||||||
|
|
||||||
|
### ✅ تصمیم مالک ۲۰۲۶-۱۰-۰۷ — محافظ در لایهی سرویس
|
||||||
|
|
||||||
|
> «تست واحد الزامی است؛ اگر متد کرش کند، باید در لایه سرویس مقصد چک شود
|
||||||
|
> که اگر فرمت هش نامعتبر یا پیشوند `!` بود، مستقیماً بدون فراخوانی bcrypt
|
||||||
|
> خروجی false برگردد.»
|
||||||
|
|
||||||
|
**پذیرفته شد — و این روش از تستکردن تنها بهتر است**، چون به رفتار
|
||||||
|
کتابخانه وابسته نمیماند:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// قبل از هر فراخوانی bcrypt
|
||||||
|
if (!passwordHash || !passwordHash.startsWith('$2')) {
|
||||||
|
return { ok: false }; // اکانت فقط-OTP یا هش نامعتبر
|
||||||
|
}
|
||||||
|
return { ok: await bcrypt.compare(dto.password, passwordHash) };
|
||||||
|
```
|
||||||
|
|
||||||
|
جای اعمال: `auth.service.ts:103` و `:135` (هر دو مسیر `verifyPassword`).
|
||||||
|
|
||||||
|
⚠️ **این محافظ از `''` فعلی هم محافظت میکند** — یعنی باگی را میبندد
|
||||||
|
که همین حالا وجود دارد، مستقل از مهاجرت.
|
||||||
|
|
||||||
|
🔶 **تست واحد همچنان لازم است** (خواست مالک)، ولی نقشش عوض شد: دیگر
|
||||||
|
«آیا bcrypt کرش میکند؟» نیست، بلکه «آیا محافظ قبل از bcrypt جلو را
|
||||||
|
میگیرد؟». در این محیط نتوانستم اجرا کنم (بررسیکنندهی سمت سرور خطا
|
||||||
|
داد)، پس **ادعا نمیکنم تست شده است**.
|
||||||
|
|
||||||
|
### 🔶 تسک رفکتور جداگانه — ثبت شد
|
||||||
|
|
||||||
|
> «تغییر آن پنج جا در اسکوپ پایپلاین مهاجرت نیست، اما به عنوان یک تسک
|
||||||
|
> رفکتور ثبت شود تا در یک مرج ریکوئست جداگانه اصلاح گردد.»
|
||||||
|
|
||||||
|
**ثبت شد.** پنج جایی که `passwordHash: ''` مینویسند:
|
||||||
|
|
||||||
|
| فایل | خط |
|
||||||
|
|---|---|
|
||||||
|
| `customer-auth.service.ts` | ۲۶۱ |
|
||||||
|
| `admin-customers.service.ts` | ۶۲۶ · ۶۹۰ |
|
||||||
|
| `accounting-sync.service.ts` | ۲۱۱ |
|
||||||
|
| `admin-branches.service.ts` | ۳۳۰ |
|
||||||
|
|
||||||
|
**خارج از اسکوپ مهاجرت.** PR جداگانه، بعد از مهاجرت.
|
||||||
|
|
||||||
⚠️ آن ۵ رمز `$2y$` (bcrypt) هستند و مقصد هم bcrypt است، پس **بدون
|
⚠️ آن ۵ رمز `$2y$` (bcrypt) هستند و مقصد هم bcrypt است، پس **بدون
|
||||||
تغییر منتقل میشوند**.
|
تغییر منتقل میشوند** و همانطور کار میکنند.
|
||||||
|
|
||||||
### enum ها — دو نکته
|
### enum ها — دو نکته
|
||||||
|
|
||||||
@@ -1064,6 +1332,136 @@ orders_orders 520 live retail_sts: 7 → ۳ ردیف
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ۱۰.۵.۵ — فهرست کامل تغییرات افزایشی مقصد
|
||||||
|
|
||||||
|
همهی تصمیمهای مالک تا ۲۰۲۶-۱۰-۰۷، در یک migration جمع میشوند.
|
||||||
|
**همه افزایشیاند** — هیچ ستونی حذف یا تغییر نوع نمیدهد، پس با
|
||||||
|
`PROTECTED.md` §G و قابلیتهای حسابداری/تصویر واریانت/پیامک **تضادی
|
||||||
|
ندارند**.
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model Customer {
|
||||||
|
nationalCode String? @unique // بود: String — §۱۰.۴
|
||||||
|
birthDate DateTime? // legacy birth_date — §۵.۲
|
||||||
|
verificationRejectionReason String? // §۵.۵
|
||||||
|
verificationRejectedAt DateTime?
|
||||||
|
// cityNameRaw String? // §۵.۹ — منتظر جواب کوئری سوم
|
||||||
|
favorites CustomerFavorite[] // §۱۰
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VerificationStep {
|
||||||
|
UNVERIFIED
|
||||||
|
STEP_ONE_SUBMITTED
|
||||||
|
STEP_TWO_SUBMITTED
|
||||||
|
VERIFIED
|
||||||
|
REJECTED // §۵.۵
|
||||||
|
}
|
||||||
|
|
||||||
|
model CustomerFavorite { // §۱۰ — legacy fav
|
||||||
|
customerId String
|
||||||
|
customer Customer @relation(fields: [customerId], references: [id])
|
||||||
|
productId String
|
||||||
|
product Product @relation(fields: [productId], references: [id])
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
@@id([customerId, productId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
و یک تغییر کد (نه schema) در `auth.service.ts:103,135` — محافظ پیشوند
|
||||||
|
`$2` قبل از `bcrypt.compare` (§۱۰.۴).
|
||||||
|
|
||||||
|
⚠️ طبق D14: `migrate dev` در توسعه، و SQL **idempotent**
|
||||||
|
(`IF NOT EXISTS`) چون کانتینر مهاجرتها را خودش اجرا میکند (D15).
|
||||||
|
|
||||||
|
### ✅ وضعیت اجرا — ۲۰۲۶-۱۰-۰۷
|
||||||
|
|
||||||
|
شاخه: `migration/legacy-data` (از `19d490e`؛ `release/delivery` دست نخورد).
|
||||||
|
|
||||||
|
| مورد | وضعیت | تأیید |
|
||||||
|
|---|---|---|
|
||||||
|
| migration `20261007120000_migration_prep_additive` | ✅ اعمال شد | `migrate diff` دیتابیس زنده ↔ schema: **خالی** |
|
||||||
|
| ۴ ستون `Customer` | ✅ همه nullable | `information_schema` |
|
||||||
|
| `CustomerFavorite` | ✅ ساخته شد | `to_regclass` |
|
||||||
|
| `VerificationStep.REJECTED` | ✅ | `pg_enum` · Postgres 15.19 |
|
||||||
|
| نوع پارامتر `accounting-sync.service.ts:233` | ✅ `string \| null` | رفتار درگاهها صفر تغییر |
|
||||||
|
| محافظ `$2` در `auth.service.ts` | ✅ `compareLocalHash()` | ۴ تست تازه |
|
||||||
|
| ممیزی مصرفکنندگان `REJECTED` | ✅ | پایین |
|
||||||
|
| تستها | ✅ **۱۷۸ از ۱۷۸** | ۱ suite از قبل خراب — پایین |
|
||||||
|
| API زنده | ✅ | عمومی ۲۰۰ · ادمین بیتوکن ۴۰۱ · رمز غلط ۴۰۱ · رمز درست ۲۰۰ |
|
||||||
|
|
||||||
|
### 🔴 خطر فرایندی کشفشده — فایل migration = اعمال خودکار
|
||||||
|
|
||||||
|
**migration بدون تأیید اعمال شد — نه توسط من.** زنجیره:
|
||||||
|
|
||||||
|
1. سرور توسعه با `nest start --watch` اجرا میشود
|
||||||
|
2. ویرایش هر فایلی زیر `src/` سرور را ریاستارت میکند
|
||||||
|
3. در boot، [`bootstrap.ts:35`](../apps/api/src/prisma/bootstrap.ts) تابع
|
||||||
|
`runMigrations()` را صدا میزند که **هر فایل معلق در `prisma/migrations`
|
||||||
|
را اعمال میکند** (D15)
|
||||||
|
|
||||||
|
ویرایش `accounting-sync.service.ts` (برای رفع خطای نوع) ریاستارت را زد و
|
||||||
|
migration در `13:42:36` در `_goldbank_migrations` ثبت شد — پیش از تأیید
|
||||||
|
مالک. نتیجه درست و تأییدشده است، ولی **روال «اول نشان بده، بعد اجرا کن»
|
||||||
|
(D0) روی این ماشین کار نمیکند**.
|
||||||
|
|
||||||
|
**قاعده از این پس:** تا تأیید نشده، migration **بیرون از
|
||||||
|
`prisma/migrations`** نوشته شود (مثلاً `docs/` یا scratchpad)، یا سرور
|
||||||
|
توسعه متوقف باشد. فقط پس از تأیید به پوشه منتقل شود.
|
||||||
|
|
||||||
|
### ⚠️ دفتر CLI پریزما ۹ migration عقب است (از قبل)
|
||||||
|
|
||||||
|
`prisma migrate status` نُه migration را «اعمالنشده» میداند، از
|
||||||
|
`20260922000000_customer_otp` تا همین تازه — چون این ماشین migration ها را
|
||||||
|
از مسیر runner خود اپ (`_goldbank_migrations`) گرفته، نه CLI. دیتابیس
|
||||||
|
کامل است (diff خالی). **پیامد:** روی این ماشین `prisma migrate dev` اجرا
|
||||||
|
نشود — میخواهد ۹ فایل را دوباره اجرا کند. همگامسازی با
|
||||||
|
`prisma migrate resolve --applied <name>` برای هر ۹ مورد ممکن است، ولی در
|
||||||
|
`_prisma_migrations` مینویسد و **تأیید مالک میخواهد** (D14).
|
||||||
|
|
||||||
|
### ممیزی مصرفکنندگان `REJECTED`
|
||||||
|
|
||||||
|
| جا | پیش از رفع | وضعیت |
|
||||||
|
|---|---|---|
|
||||||
|
| `admin-customers.service.ts` `verifyStepOne()` | 🔴 `default` → `2` (**پذیرفته**). نما سوییچهای سفارش/حساب را روی `step_one === 2` باز میکند → **مشتری ردشده سوییچ باز میگرفت** | ✅ صریحاً `3` |
|
||||||
|
| همان، `verifyStepTwo()` | `default` → `3` (درست، تصادفی) | ✅ صریح |
|
||||||
|
| `ProfileView.vue` هر دو اپ | برچسب نداشت → کلمهی انگلیسی `REJECTED` نمایش داده میشد | ✅ «رد شده» |
|
||||||
|
| فیلتر «تاییدنشدهها» (`:221`) | فقط `STEP_ONE/TWO_SUBMITTED` | ✅ بدون تغییر — ردشدهها **در صف برنمیگردند** (خواست مالک) |
|
||||||
|
| روتر هر دو اپ · دکمهی تکمیل پروفایل | ردشده مثل `UNVERIFIED` رفتار میشود | ✅ مطابق legacy — پایین |
|
||||||
|
|
||||||
|
`default` در هر دو تابع **حذف شد** تا switch جامع باشد: اگر روزی مقدار
|
||||||
|
ششمی به enum اضافه شود، TypeScript خطا میدهد و همین باگ بیصدا تکرار
|
||||||
|
نمیشود.
|
||||||
|
|
||||||
|
### 🔶 دو سؤال برای مالک — از ممیزی
|
||||||
|
|
||||||
|
**۱. رد در سیستم جدید `REJECTED` نمینویسد.**
|
||||||
|
`verify()` در `admin-customers.service.ts:955-969` هنگام رد:
|
||||||
|
گام ۱ → `UNVERIFIED`، گام ۲ → `STEP_ONE_SUBMITTED`. یعنی ۱۴ مشتری مهاجر
|
||||||
|
`REJECTED` میشوند، ولی **هر ردِ تازه در پنل همان کاری را میکند که مالک
|
||||||
|
برای مهاجرت «کاملاً مردود» خواند** (برگشت به صف). و
|
||||||
|
`verificationRejectionReason` که مالک «برای نیازمندیهای آتی» خواست، از
|
||||||
|
هیچ مسیری پر نمیشود — فرم ادمین فیلد علت ندارد.
|
||||||
|
پیشنهاد: `verify()` هنگام رد `REJECTED` + `verificationRejectedAt`
|
||||||
|
بنویسد، و یک فیلد اختیاری علت به دکمهی رد اضافه شود. **تغییر رفتار یک
|
||||||
|
جریان زنده است — تأیید لازم.**
|
||||||
|
|
||||||
|
**۲. مشتری ردشده با ارسال دوبارهی مدارک خودکار تأیید میشود.**
|
||||||
|
legacy دکمهی تکمیل پروفایل را فقط وقتی `sts_verify_step_two` برابر ۱ یا
|
||||||
|
۲ بود پنهان میکرد (طبق کامنت پورت در `ProfileView.vue:122-126`)، پس ردشده (۳) آن را
|
||||||
|
میدید؛ و ارسال، بیدرنگ `VERIFIED` میکند چون legacy خط «منتظر
|
||||||
|
کارشناس» را کامنت کرده بود (`customer-profile.service.ts:16-19`). پورت
|
||||||
|
همین است. **نتیجه: «رد» در عمل یک «دوباره بفرست» است، نه بستن پرونده.**
|
||||||
|
اگر مالک میخواهد رد واقعاً ببندد، این تصمیم محصولی است، نه پورت.
|
||||||
|
|
||||||
|
### ⚠️ `sms.service.spec.ts` از قبل خراب است
|
||||||
|
|
||||||
|
دو خطای `TS2339: Property 'platform' does not exist` (خطوط ۸۳، ۱۰۲). این
|
||||||
|
فایل و `sms.service.ts` در این شاخه **دست نخوردهاند** — خرابی در commit
|
||||||
|
`19d490e` است. به همین دلیل آن suite کامپایل نمیشود؛ ۱۷۸ تست بقیه سبزند.
|
||||||
|
خارج از اسکوپ این کار، ثبت میشود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ۱۰.۶ — پرسش دوم از دیتابیس
|
## ۱۰.۶ — پرسش دوم از دیتابیس
|
||||||
|
|
||||||
📄 **[`migration-precheck-2.sql`](./migration-precheck-2.sql)** — چهار سؤالی
|
📄 **[`migration-precheck-2.sql`](./migration-precheck-2.sql)** — چهار سؤالی
|
||||||
@@ -1087,10 +1485,10 @@ orders_orders 520 live retail_sts: 7 → ۳ ردیف
|
|||||||
| # | مورد | § |
|
| # | مورد | § |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| ۱ | ~~شمارش تکرار~~ | ✅ **انجام شد** — ۲۷ گروه موبایل، ۰ کد ملی (§۱۰.۴) |
|
| ۱ | ~~شمارش تکرار~~ | ✅ **انجام شد** — ۲۷ گروه موبایل، ۰ کد ملی (§۱۰.۴) |
|
||||||
| ۱.۱ | 🔴 **`nationalCode` باید nullable شود** — هر ۱٬۵۹۵ ردیف تهی است | ۱۰.۴ |
|
| ۱.۱ | ~~`nationalCode`~~ | ✅ **حل — nullable شود** (§۱۰.۴) |
|
||||||
| ۱.۲ | 🔴 **`passwordHash`** — ۱٬۵۹۰ تهی؛ hash تصادفی یا nullable | ۱۰.۴ |
|
| ۱.۲ | ~~`passwordHash`~~ | ✅ **حل — hash غیرقابلورود** (§۱۰.۴) |
|
||||||
| ۱.۳ | 🔴 **مبلغ سند مالی کجاست؟** `cashes.remain` همه `NULL` | ۱۰.۶ |
|
| ۱.۳ | 🔴 **مبلغ سند مالی کجاست؟** `cashes.remain` همه `NULL` | ۱۰.۶ |
|
||||||
| ۱.۴ | 🔴 **`VerificationStep` حالت `REJECTED` ندارد** — ۱۴ ردیف | ۵.۵ |
|
| ۱.۴ | ~~`REJECTED`~~ | ✅ **حل — enum + دو ستون علت** (§۵.۵) |
|
||||||
| ۱.۵ | 🔴 **ترتیب دومرحلهای** — کارشناسان قبل از مشتریان | ۵.۷ |
|
| ۱.۵ | 🔴 **ترتیب دومرحلهای** — کارشناسان قبل از مشتریان | ۵.۷ |
|
||||||
| ۲ | آستانهی تطبیق عددی جای «صفر اختلاف» — **تأیید شد: ۵۶ ردیف گِرد میشوند** | ۴ · ۱۰.۴ |
|
| ۲ | آستانهی تطبیق عددی جای «صفر اختلاف» — **تأیید شد: ۵۶ ردیف گِرد میشوند** | ۴ · ۱۰.۴ |
|
||||||
| ۳ | قاعدهی `WHERE deleted_at IS NULL` — **حیاتیتر از انتظار: ۴۵٪ واریانتها** | ۵.۸ · ۱۰.۴ |
|
| ۳ | قاعدهی `WHERE deleted_at IS NULL` — **حیاتیتر از انتظار: ۴۵٪ واریانتها** | ۵.۸ · ۱۰.۴ |
|
||||||
@@ -1103,8 +1501,9 @@ orders_orders 520 live retail_sts: 7 → ۳ ردیف
|
|||||||
|
|
||||||
| # | مورد | § |
|
| # | مورد | § |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| ۱ | ۴ ستون دادهی غیرقابلبازسازی (`birth_date`, `marriage_date`, `token_mobile`, `new_city`) | ۵.۲ |
|
| ۱ | ~~۴ ستون دادهی غیرقابلبازسازی~~ | ✅ **حل — تولد و شهر منتقل، توکن و سالگرد حذف** (§۵.۲) |
|
||||||
| ۲ | `verificationStep` — تفکیک `STEP_TWO_SUBMITTED` از `VERIFIED` | ۵.۵ |
|
| ۱.۱ | 🔶 `new_city` → `cityId` یا ستون متنی؟ | **۵.۹** |
|
||||||
|
| ۲ | ~~تفکیک `STEP_TWO_SUBMITTED` از `VERIFIED`~~ | ✅ **حل** (§۵.۵) |
|
||||||
| ۳ | `type IS NULL` چه `CustomerKind` شود | ۵.۳ |
|
| ۳ | `type IS NULL` چه `CustomerKind` شود | ۵.۳ |
|
||||||
| ۴ | ~~`fav`~~ | ✅ **حل — لازم است، migration افزایشی** (§۱۰) |
|
| ۴ | ~~`fav`~~ | ✅ **حل — لازم است، migration افزایشی** (§۱۰) |
|
||||||
| ۴.۱ | `cinies`/`ciny_products` — چیدمان نمایشی | ۱۰ |
|
| ۴.۱ | `cinies`/`ciny_products` — چیدمان نمایشی | ۱۰ |
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- پیشبررسی سوم — فقط دو سؤال باقیمانده
|
||||||
|
-- ============================================================================
|
||||||
|
--
|
||||||
|
-- ⚠️ فقط SELECT COUNT. صفر INSERT/UPDATE/DELETE. هیچ ستون متنی در خروجی.
|
||||||
|
--
|
||||||
|
-- اجرا:
|
||||||
|
-- mysql -u USER -p -h HOST -P 3305 oapi < migration-precheck-3.sql > out3.txt
|
||||||
|
--
|
||||||
|
-- مرجع: docs/DATA-MIGRATION-MAP.md §۵.۹
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- سؤال: new_city باید به cityId نگاشت شود یا ستون جدا بگیرد؟
|
||||||
|
--
|
||||||
|
-- تصمیم مالک: «یا در فیلد شهر بنشیند یا در یک فیلد آدرس متنی مپ شود».
|
||||||
|
-- کدامیک، به این بستگی دارد که آیا این ۱۷۶ ردیف city_id هم دارند یا نه:
|
||||||
|
--
|
||||||
|
-- فقط new_city (city_id تهی) → ستون متنی لازم است، چون شهر در جدول cities نیست
|
||||||
|
-- هر دو را دارند → new_city تکراری است، میتوان رهایش کرد
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
SELECT '=== new_city vs city_id ===' AS section;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
SUM(new_city IS NOT NULL AND new_city <> '' AND city_id IS NULL) AS only_new_city,
|
||||||
|
SUM(new_city IS NOT NULL AND new_city <> '' AND city_id IS NOT NULL) AS both,
|
||||||
|
SUM((new_city IS NULL OR new_city = '') AND city_id IS NOT NULL) AS only_city_id,
|
||||||
|
SUM((new_city IS NULL OR new_city = '') AND city_id IS NULL) AS neither
|
||||||
|
FROM customers WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- از آن ۱۷۶ ردیف، چندتا متنیشان با عنوان یک شهر موجود در `cities` یکی است؟
|
||||||
|
-- اگر بالا باشد، اکثرشان قابل تبدیل به city_id هستند و ستون متنی لازم نیست.
|
||||||
|
SELECT 'new_city matches an existing cities.title' AS metric, COUNT(*) AS n
|
||||||
|
FROM customers c
|
||||||
|
JOIN cities ci ON TRIM(ci.title) = TRIM(c.new_city)
|
||||||
|
WHERE c.deleted_at IS NULL AND c.new_city IS NOT NULL AND c.new_city <> ''
|
||||||
|
AND c.city_id IS NULL;
|
||||||
|
|
||||||
|
-- چند مقدار متمایز؟ (عدد است، نه متن — اگر کم باشد نگاشت دستی ممکن است)
|
||||||
|
SELECT 'distinct_new_city_values' AS metric, COUNT(DISTINCT TRIM(new_city)) AS n
|
||||||
|
FROM customers WHERE deleted_at IS NULL AND new_city IS NOT NULL AND new_city <> '';
|
||||||
|
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- سؤال: birth_date — آیا تاریخها معتبرند؟
|
||||||
|
--
|
||||||
|
-- ستون timestamp است. ولی اگر تاریخ جلالی بهاشتباه بهعنوان میلادی
|
||||||
|
-- ذخیره شده باشد، سالها ۱۳xx میشوند که در timestamp جا نمیشوند،
|
||||||
|
-- یا به مقادیر عجیب تبدیل شدهاند.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
SELECT '=== birth_date sanity ===' AS section;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COUNT(birth_date) AS total_set,
|
||||||
|
SUM(YEAR(birth_date) < 1900) AS year_before_1900,
|
||||||
|
SUM(YEAR(birth_date) BETWEEN 1900 AND 1950) AS year_1900_1950,
|
||||||
|
SUM(YEAR(birth_date) BETWEEN 1951 AND 2010) AS year_1951_2010,
|
||||||
|
SUM(YEAR(birth_date) > 2010) AS year_after_2010,
|
||||||
|
CAST(MIN(YEAR(birth_date)) AS CHAR) AS min_year,
|
||||||
|
CAST(MAX(YEAR(birth_date)) AS CHAR) AS max_year
|
||||||
|
FROM customers WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
SELECT '=== END ===' AS section;
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
| `docs/GO-LIVE-PLAN.md` | Stack upgrade, release isolation, data migration and cutover from the live henza system. **§0 explains how to continue on another machine.** | Owner go-ahead per step |
|
| `docs/GO-LIVE-PLAN.md` | Stack upgrade, release isolation, data migration and cutover from the live henza system. **§0 explains how to continue on another machine.** | Owner go-ahead per step |
|
||||||
| `docs/PWA-PORT-PLAN.md` | Third user app `apps/user-app-classic`: a pixel-exact port of `henza_pwa@b171fff` onto the new API. | Owner rulings on its tensions 1–5 and Q1–Q9 |
|
| `docs/PWA-PORT-PLAN.md` | Third user app `apps/user-app-classic`: a pixel-exact port of `henza_pwa@b171fff` onto the new API. | Owner rulings on its tensions 1–5 and Q1–Q9 |
|
||||||
| `docs/ACCOUNTING-INTEGRATION-PLAN.md` | Tahesab / Zarrin integration, switchable in the admin panel (D27). **Implemented 2026-10-05**, off until switched on; progress in its §10. | Zarrin trial on a real install (ACC-01); ACC-03 ruling |
|
| `docs/ACCOUNTING-INTEGRATION-PLAN.md` | Tahesab / Zarrin integration, switchable in the admin panel (D27). **Implemented 2026-10-05**, off until switched on; progress in its §10. | Zarrin trial on a real install (ACC-01); ACC-03 ruling |
|
||||||
|
| `docs/DATA-MIGRATION-MAP.md` | Column-by-column map of the live henza MySQL schema (`oapi-struncture.sql`, 80 tables) onto Prisma, checked against real row counts via the read-only `docs/migration-precheck*.sql`. Branch `migration/legacy-data`. **2026-10-07:** additive prep migration applied (nullable `nationalCode`, `birthDate`, rejection trail + `REJECTED`, `CustomerFavorite`), bcrypt guard, REJECTED consumer audit. ⚠ On this machine a file in `prisma/migrations` is applied by the watch-mode dev server on the next `src/` edit — see its §10.5.5. | `precheck-2` (where the ledger amount lives) and `precheck-3` output; two `verify()` rulings |
|
||||||
|
|
||||||
The "Current state" table below is the parity project's and was last updated 2026-09-22.
|
The "Current state" table below is the parity project's and was last updated 2026-09-22.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user