docs: migration map — verified data facts, corrections, import tool

DATA-MIGRATION-MAP.md:
- §10.7 is rewritten from the full dump loaded into MySQL. The legacy
  debt formula is confirmed (1,590/1,591 balances). The earlier claims
  that new_city is a cities5 id, that birthdays are clean and that
  types 3/4/5 are unknown were wrong and are corrected in a table.
  Favourites come from major_favorites, not fav. The role-number clash
  (legacy 86 «حسابدار» vs target 86 «کارشناس فروش») is recorded.
- §10.8 adds the tool's usage, safety rules, test result, and what it
  deliberately does not migrate.
- §11 and §5.5 are brought up to date. The rejected count is 7, not 14.

migration-precheck-3.sql: two bugs that would have failed on the server
are fixed: the reserved word `both` and a collation clash between
cities.title and customers.new_city.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-10-07 19:35:19 +03:30
co-authored by Claude Opus 5.5
parent be5ca829d2
commit 92831ed33e
2 changed files with 242 additions and 21 deletions
+238 -19
View File
@@ -3,9 +3,10 @@
> سند **نگاشت**، نه پلن اجرا. هیچ کدی بر اساس این سند نوشته نشده.
> پیش‌نیاز ابزار مهاجرت داده‌ی [`GO-LIVE-PLAN.md`](./GO-LIVE-PLAN.md) §۷.
**منبع:** `oapi-struncture.sql` — دامپ Navicat، تاریخ ۰۵/۱۰/۲۰۲۶
**منبع اول:** `oapi-struncture.sql` — دامپ Navicat فقط ساختار، تاریخ ۰۵/۱۰/۲۰۲۶
**منبع دوم:** `oapi.sql` — دامپ کامل با داده، تاریخ ۰۴/۱۰/۲۰۲۶، **۲۷۴٬۷۷۳ سطر**
**مقصد:** `apps/api/prisma/schema.prisma` — ۴۳ مدل، ۴۷۷ فیلد
**تاریخ نگارش:** ۱۴ مهر ۱۴۰۵ (۲۰۲۶-۱۰-۰۶)
**تاریخ نگارش:** ۱۴ مهر ۱۴۰۵ (۲۰۲۶-۱۰-۰۶) · به‌روز ۱۵ مهر ۱۴۰۵ (دیتای واقعی)
---
@@ -19,7 +20,22 @@
| جداول | **۸۰** |
| ستون‌ها | **۱٬۰۸۷** |
| Engine | `InnoDB` (هر ۸۰ جدول) |
| محتوا | **فقط ساختار — صفر `INSERT`** |
| محتوا | **دامپ کامل — `oapi.sql` دارد `INSERT`** |
### ۰.۱ شمارش ردیف‌های کلیدی (از `oapi.sql`)
| جدول | ردیف | یادداشت |
|---|---|---|
| `customers` | **۱٬۶۱۲** | `AUTO_INCREMENT=1720` — ۱۰۸ شکاف |
| `cashes` | **۶۴۸** | اسناد دفتر مالی |
| `products_productvarient` | **۷٬۴۴۳** | شامل حذف‌شده‌ها |
| `product` | **۳٬۲۷۲** | محصولات |
| `image` | **۱۷٬۹۵۵** | فایل‌های فیزیکی: ۱۷٬۸۲۶ (در `storage/`) |
| `products_productimage` | **۵٬۴۱۷** | رابطه product→image |
| `users` | **۲۲** | ادمین/کارشناسان |
| `orders_orders` | **۵۲۰** | سفارش‌ها (به تأخیر افتاد §۷.۲) |
| `wallets` | **۸۰** | کیف‌پول خرده‌فروشی |
| `fav` | **۲۵۱** | مورد علاقه (migration شد §۱۰) |
این فایل شکافی را پر کرد که قبلاً باز بود: جداول دوران Django
(`product`، `products_productvarient`، `category_collection` و …) هیچ migration
@@ -462,17 +478,20 @@ model Customer {
| | |
|---|---|
| ستون `verificationRejectionReason` | ✅ **ساخته می‌شود** (خواست مالک · برای آینده) |
| مقدارش در مهاجرت ۱۴ ردیف | **`NULL`** — چون مبدأ داده‌ای ندارد |
| مقدارش در مهاجرت **۷ ردیف** | متن ثابت مالک (پایین) — علت واقعی در مبدأ نیست |
| `verificationRejectedAt` | **`NULL`** — legacy زمان رد را هم ثبت نمی‌کرد |
⚠️ **این یک شکاف داده‌ی ذاتی است، نه نقص ابزار مهاجرت.** علت رد آن ۱۴
⚠️ **این یک شکاف داده‌ی ذاتی است، نه نقص ابزار مهاجرت.** علت رد آن ۷
مشتری در سیستم قدیم هرگز ذخیره نشده بود و **قابل بازیابی نیست**. ستون
ساخته می‌شود تا از این پس پر شود؛ برای این ۱۴ نفر خالی می‌ماند.
ساخته می‌شود تا از این پس پر شود.
### ✅ تصمیم مالک ۲۰۲۶-۱۰-۰۷ — یادداشت خودکار درج شود
> «فیلد علت برای این ۱۴ ردیف نباید کامپلت NULL رها شود.»
(عدد درست **۷** است: ۲ رد در گام دو، ۳ در گام یک، ۲ در هر دو. عدد ۱۴ را
من اشتباه جمع کرده بودم و مالک بر اساس آن نوشت؛ تصمیم تغییری نمی‌کند.)
مقدار دقیق که ابزار درج می‌کند:
```
@@ -941,10 +960,16 @@ legacy شش ستون برای نسخه‌های تغییراندازه‌یاف
| `request_fashions` | `name`, `mobile` | درخواست «مد» — فرم تماس | ❌ ندارد |
| `request_logs` | `customer_id`, `ip`, `device`, `data`, `user_agent`, `route` | لاگ درخواست | تقریباً `CustomerActivityLog` |
### ✅ تصمیم مالک ۲۰۲۶-۱۰-۰۶ — `fav` لازم است
### ✅ تصمیم مالک ۲۰۲۶-۱۰-۰۶ — علاقه‌مندی‌ها لازم‌اند
> «بله» — علاقه‌مندی‌ها مهم‌اند.
🔴 **تصحیح ۲۰۲۶-۱۰-۰۷: منبع درست `fav` نیست.** `fav` مال فروشگاه خرده است
(`Customer\FavController`، guard `api-singleCustomer`). اپ عمده
**`major_favorites`** (۳٬۳۰۵ ردیف) را می‌خواند — §۱۰.۷. متن زیر درباره‌ی
`fav` نوشته شده؛ تصمیم مالک و مدل `CustomerFavorite` همان می‌مانند، فقط منبع
عوض شد.
**یک migration افزایشی لازم است.** چون additive است، با `PROTECTED.md` §G
تضادی ندارد و قابلیت موجودی را لمس نمی‌کند.
@@ -1436,7 +1461,7 @@ migration در `13:42:36` در `_goldbank_migrations` ثبت شد — پیش ا
**۱. رد در سیستم جدید `REJECTED` نمی‌نویسد.**
`verify()` در `admin-customers.service.ts:955-969` هنگام رد:
گام ۱ → `UNVERIFIED`، گام ۲ → `STEP_ONE_SUBMITTED`. یعنی ۱۴ مشتری مهاجر
گام ۱ → `UNVERIFIED`، گام ۲ → `STEP_ONE_SUBMITTED`. یعنی ۷ مشتری مهاجر
`REJECTED` می‌شوند، ولی **هر ردِ تازه در پنل همان کاری را می‌کند که مالک
برای مهاجرت «کاملاً مردود» خواند** (برگشت به صف). و
`verificationRejectionReason` که مالک «برای نیازمندی‌های آتی» خواست، از
@@ -1471,13 +1496,202 @@ legacy دکمه‌ی تکمیل پروفایل را فقط وقتی `sts_verify_
| # | سؤال | چرا مسدودکننده است |
|---|---|---|
| ۱ | **مبلغ سند مالی در کدام ستون است؟** | `remain`/`rial_remain` همه `NULL`اند. بدون جواب، `LedgerEntry.amount` خالی می‌ماند و **کل دفتر مالی بی‌مبلغ منتقل می‌شود** |
| ۱ | ~~مبلغ سند مالی در کدام ستون است؟~~ | ✅ **حل — §۱۰.۷** |
| ۲ | آیا «مسئول فروش برنده» برای ۲۷ گروه تکراری کافی است؟ | اگر گروهی کارشناس نداشته باشد، قاعده بی‌اثر است |
| ۳ | سه ستون تصویر → چهار نقش `ProductImageRole` | نگاشت غلط = تصویر غلط روی محصول |
| ۴ | تأیید §۵.۷ — `reagent_id` به `customers` اشاره می‌کند؟ | اگر غلط باشد، رابطه‌ی کارشناس معرف از بین می‌رود |
---
## ۱۰.۷ — یافته‌های دامپ کامل (`oapi.sql` — ۲۰۲۶-۱۰-۰۷)
`oapi.sql` (دامپ Navicat با داده، تاریخ ۰۴/۱۰/۲۰۲۶، ۶۳ مگابایت) در یک MySQL
محلی روی Docker بارگذاری و هر ادعا با کوئری روی همان داده بررسی شد. این
بخش ابتدا توسط نشست موازی `gold-bank-79` نوشته شد. **چند ادعای آن نسخه
غلط بود و این‌جا تصحیح شده است** (جدول پایین).
⚠️ دامپ **دو روز قدیمی‌تر** از اجرای پیش‌بررسی روی سرور زنده است (مثلاً
۷٬۴۴۳ تنوع این‌جا در برابر ۷٬۴۷۴ زنده). برای ساخت و آزمون کافی است.
**برای مهاجرت واقعی دامپ تازه گرفته شود.**
`oapi.sql` داده‌ی شخصی مشتریان است: در `.gitignore` قرار گرفت و هرگز commit
نشده است.
### تصحیح ادعاهای نسخه‌ی قبلی این بخش
| ادعای قبلی | واقعیت (کوئری روی داده) |
|---|---|
| `remain`/`rial_remain` صفرند | **NULL** هستند — در هر ۳۸۳ ردیف زنده؛ ساختار `DEFAULT NULL` دارد |
| `new_city` شناسه‌ی عددی `cities5` است | **هر ۱۷۲ مقدار نام شهرِ تایپ‌شده است؛ صفر مقدار عددی** — §۵.۹ |
| `birth_date` همه بین ۱۹۵۰ و ۲۰۱۰، بدون نیاز به پالایش | **۳۵۷ مورد بعد از ۲۰۱۰ تا ۲۰۳۱؛ ۱۹۰ مورد برابر تاریخ ثبت‌نام** |
| `type` ۳ و ۴ و ۵ نامعلوم، نیاز به تأیید مالک | در `Customer::$TYPES` (Customer.php:85-92) تعریف شده‌اند — §۵.۳ |
| `LedgerEntry.goldGrams = value`، `rialAmount = price` | این فیلدها وجود ندارند؛ مدل `amount` + `currency` + `direction` است |
| شمارش انواع سند: ۹ → ۴۸۲، ۲ → ۶۳، … | شامل حذف‌شده‌ها بود؛ زنده: ۹ → ۲۸۰، ۲ → ۳۷، ۱ → ۳۳، ۴ → ۳۲، ۳ → ۱ |
| علاقه‌مندی = `fav` (۲۵۱) | `fav` مال فروشگاه خرده است (guard `api-singleCustomer`)؛ اپ عمده **`major_favorites`** (۳٬۳۰۵) — پایین |
**درست بود:** مبلغ سند در `value` (گرم) و `price` (ریال) است؛ پوشه‌ی تصاویر
`F:\New folder (2)\storage` با **۱۷٬۸۳۲ فایل (۱.۸ گیگابایت)** وجود دارد.
### 🔴 فرمول بدهی legacy — با داده تأیید شد
`CustomerController::updateRemain` (Api/CustomerController.php:42-51):
```
remain = Σ orders.input (sts > 4) − Σ cashes.value (confirmed = 1, type 1–4)
rial_remain = Σ orders.rial_input (sts > 4)
```
هر دو مدل `SoftDeletes` دارند، پس حذف‌شده‌ها بیرون‌اند. بازمحاسبه روی
دامپ: **طلا ۱٬۵۹۰ از ۱٬۵۹۱ مشتری، ریال ۱٬۵۹۱ از ۱٬۵۹۱** با مانده‌ی ذخیره‌شده
برابر است. جمع کل ۴۲۷٫۵۰۴۲ گرم — همان عدد پیش‌بررسی روی سرور زنده.
سه پیامد که قبلاً دیده نشده بود:
1. **سند نوع ۹ («سفارش») در بدهی نیست.** بدهی از جدول `orders` می‌آید؛ نوع ۹
فقط آینه‌ی آن در فهرست است. انتقال هر دو = **دوبار شمردن بدهی هر سفارش**.
2. **ریال هرگز بدهی ریالی را کم نمی‌کند.** پرداخت پولی (نوع ۴) با قیمت روز به
گرم تبدیل می‌شود (`price ÷ (price_per_gram ÷ 4.331802)` — قیمت هر مثقال)
و بدهی **طلا** را کم می‌کند.
3. **چک (نوع ۳) اثری در مانده ندارد** — مقدار گرمی‌اش صفر است.
**تنها استثنا — مشتری `#125`:** مانده‌ی ذخیره‌شده −۹٫۸۹۵۱ گرم، بازمحاسبه صفر.
تنها سندش (۴۸٫۳۴ گرم آب‌شده) در ۲۰۲۱ حذف شده و مشتری در ۲۰۲۲ دوباره
ویرایش شده — مانده به‌احتمال زیاد **دستی** وارد شده (ستون در `$fillable`
بود، SEC-14). ✅ **تصمیم مالک ۲۰۲۶-۱۰-۰۷:** همان −۹٫۸۹۵۱ منتقل شود، به‌صورت
یک سند صریح «تراز انتقالی».
### ✅ تاریخ تولد — پالایش (تصمیم مالک ۲۰۲۶-۱۰-۰۷)
از ۹۱۵ تاریخ ثبت‌شده فقط **۵۴۱** واقعی است:
| دسته | تعداد | چرا کنار گذاشته شد |
|---|---|---|
| برابر روز ثبت‌نام | ۱۹۰ | پیش‌فرض فرم — در سال ۲۰۲۵ از ۱۷۶ مورد، ۱۶۵ |
| ساعت دارد یا سال بعد از ۲۰۰۸ | ۱۸۴ | تاریخ تولد نمی‌تواند باشد |
| **نگه داشته شد** | **۵۴۱** | |
### ✅ شهر — نگاشت دوگانه (تصمیم مالک ۲۰۲۶-۱۰-۰۷)
ثبت‌نام اپ روی **همه** `city_id = 5` (تهران) می‌گذاشت
(Major/CustomerController.php:21) و شهر واقعی را در `new_city` به‌صورت
متن. از ۹۲ مشتری که هر دو را دارند، ۹۱ همان تهرانِ اجباری‌اند.
- متن تایپ‌شده هست → عیناً در `Customer.cityNameRaw`؛ `cityId` فقط با تطابق
دقیق نام (پس از یکسان‌سازی «ي/ك») — تهرانِ اجباری نادیده گرفته می‌شود
- متن نیست → همان `city_id` قدیم
یکسان‌سازی «ي/ك» اهمیت داشت: تطابق با جدول ۱۵تایی از ۳۲ به ۷۲ رسید.
### نقش‌ها — تداخل شماره (`config_statics`)
نوع کاربر در legacy یک ردیف `config_statics` است (UserController.php:26).
عناوین: ۸۵ مدیر · ۸۶ **حسابدار** · ۱۰۲ ویرایشگر · ۱۰۴ مدیر اصلی · ۱۰۵ مدیران
· ۱۰۸ انباردار · ۱۳۲ بیکار · ۱۳۵ سئو · ۱۳۹ مدیر شعبه.
🔴 در مقصد (seed) **۸۶ = کارشناس فروش**. کپی شماره‌به‌شماره دسترسی ۷۱ گانه‌ی
حسابدار را به همه‌ی کارشناسان می‌داد. ابزار **با عنوان** نگاشت می‌کند
(۸۶ حسابدار → ۸۷ حسابدار)؛ فقط ۸۵ با ۸۵ برابر گرفته شده (هر دو نقش
تمام‌دسترسی‌اند).
۷ کارشناس فروش با موبایل یک کاربر ادمین مشترک‌اند — ۳ تا مدیر ارشد.
قاعده‌ی «کارشناس برنده» روی آن‌ها **مدیر ارشد را به کارشناس تنزل می‌داد و از
پنل بیرون می‌کرد**. ابزار نقش ادمین را نگه می‌دارد و معرفی‌ها به همان ادمین
وصل می‌شوند.
### علاقه‌مندی‌ها — جدول درست `major_favorites` است
`fav` (۲۵۱) را `Customer\FavController` زیر guard `api-singleCustomer`
(ورود مشتری خرده) می‌نویسد؛ `customer_id` آن به جدول دیگری اشاره دارد. اپ
عمده `Major\Favorite` = **`major_favorites`** (۳٬۳۰۵ ردیف) را می‌خواند
(ApiMobileController.php:73-465). ساختار `CustomerFavorite` بی‌تغییر جواب
می‌دهد؛ فقط منبع عوض شد. کامنت جدول در دیتابیس هنوز «legacy fav» می‌گوید —
در migration بعدی اصلاح شود.
### سایر یافته‌ها
- **زمان‌ها ساعت تهران‌اند** (`config/app.php:70` → `Asia/Tehran`). ابزار
با قواعد ICU تبدیل می‌کند، با ساعت تابستانی ایران تا ۲۰۲۲.
- **۴۷۳ از ۶۱۰ ردیف سفارش زنده** به تنوعِ حذف‌شده اشاره دارند → تنوع و محصول
حذف‌شده‌ی ارجاع‌شده با `isActive = false` منتقل می‌شوند.
- **همه‌ی ۱٬۲۵۰ محصولِ چنددسته** هم در درخت سایت (نوع ۱) هم اپ (نوع ۲)
هستند؛ مقصد یک `categoryId` دارد که اپ نشان می‌دهد → **دسته‌ی اپ** نگه
داشته می‌شود.
- **۲۷ گروه موبایل تکراری: در هیچ‌کدام بیش از یک ردیف پول ندارد** و هیچ
کارشناسی درگیر نیست → ادغام از نظر مالی بی‌خطر است.
- **سرپرست کارگاه** در legacy مشتری است (`belongsTo(Customer)`)، در مقصد
ادمین → پیوند ساخته نمی‌شود.
- **مشتری `#594`** به‌جای موبایل کلمه‌ی «تست» دارد؛ با یک سفارش (`#666`،
وضعیت ۱، بدون بدهی) کنار گذاشته می‌شود.
- **۶ تنوع** اجرت ریالی (`rial_pay`) دارند؛ مقصد فیلدی برایش ندارد.
- **عدد رد‌شده‌ها ۷ است، نه ۱۴** (۲ گام دو، ۳ گام یک، ۲ هر دو). عدد ۱۴ در
گزارش قبلی اشتباه جمع شده بود.
---
## ۱۰.۸ — ابزار انتقال (`apps/api/scripts/legacy-import/`)
### اجرا
```
cd apps/api
npx ts-node --transpile-only scripts/legacy-import/index.ts \
--source "mysql://USER:PASS@HOST:PORT/oapi" \
--target "postgresql://USER:PASS@HOST:5432/goldbank_import_test" \
--images "F:/New folder (2)/storage" --upload-dir "<UPLOAD_DIR مقصد>" \
--report ./legacy-import-report.md --yes
```
بدون `--yes` فقط پیش‌بررسی می‌کند و چیزی نمی‌نویسد. `--target` هرگز از
`.env` خوانده نمی‌شود. اگر نام دیتابیس مقصد `import` یا `staging` نداشته
باشد، `--confirm-db=<همان نام>` هم لازم است.
### قواعد ایمنی (همه در کد)
- منبع فقط خوانده می‌شود.
- قبل از اجرا وجود ستون‌های migration های آماده‌سازی چک می‌شود.
- هرگز نمی‌نویسد: `accounting*` مشتری و محصول، `ProductVariant.imageId`،
جداول `Accounting*`. هیچ `ProductImage`ی حذف نمی‌شود.
- ردیفی که از قبل در مقصد بوده (`matched`) فقط فیلدهای **خالی‌اش** پر می‌شود.
- مستقیم از Prisma می‌نویسد، نه از سرویس‌ها: **نه پیامکی می‌رود، نه چیزی به
نرم‌افزار حسابداری صف می‌شود.**
- نگاشت شناسه‌ها در schema جدای `legacy_import.id_map` در مقصد است (بیرون از
`public`، پس تغییر schema اپ نیست). اجرای دوباره به‌روزرسانی می‌کند، تکرار
نمی‌سازد؛ اجرایی که نیمه‌کاره ماند با اجرای بعدی تمام می‌شود. بعد از
جابه‌جایی: `DROP SCHEMA legacy_import CASCADE` — یا نگه داشته شود برای
redirect لینک‌های قدیمی محصول.
### نتیجه‌ی آزمون — ۲۰۲۶-۱۰-۰۷
مقصد: `goldbank_import_test`، کپی دیتابیس توسعه با ۵۹ مشتری و ۲۸ محصول
واقعی (تا ادغام هم آزموده شود). سه اجرای پیاپی:
| | نتیجه |
|---|---|
| مشتری ساخته‌شده | ۱٬۵۰۳ · ادغام با موجود ۱۳ · ادغام تکراری ۳۲ |
| بدهی طلا، مشتری‌های ساخته‌شده | **۱٬۵۰۳ از ۱٬۵۰۳ برابر legacy (±۰٫۰۰۰۱ گرم)** |
| بدهی ریال | **۱٬۵۰۳ از ۱٬۵۰۳** |
| جمع بدهی طلا | legacy ۴۲۷٫۵۰۳۹ · مقصد ۴۲۷٫۵۰۴۱ (دو گِرد ۰٫۰۰۰۱) |
| سند تراز انتقالی | فقط مشتری `#125`: بستانکار ۹٫۸۹۵۱ |
| داده‌ی حسابداری (۵۱ مشتری، ۱۸ محصول، ۷۴ پیوند) | **بایت‌به‌بایت یکسان** با دیتابیس توسعه |
| `imageId` روی تنوع وارداتی | **۰** |
| کارشناس | ۴۹ ساخته شد با پروفایل · ۸ حساب معاملاتی · ۷ ادمین تنزل **نیافت** |
| محصول / تنوع | ۱٬۴۷۳ / ۱٬۶۱۲ (شامل ۲۱۵ / ۳۴۷ حذف‌شده‌ی غیرفعال برای تاریخچه) |
| سفارش / ردیف | ۲۹۹ از ۳۰۰ (`#666` مشتری تست) / ۶۰۸ |
| سند مالی | ۸۶ بدهی سفارش + ۹۵ پرداخت تأییدشده + ۸ تأییدنشده |
| علاقه‌مندی | ۸۷۴ (۵۳۴ به محصول حذف‌شده و ۵۹ به مشتری منتقل‌نشده کنار رفت) |
| سفارش ساختنی | ۲۰ از ۲۰ |
| تصویر | ۶٬۱۹۶ فایل (۵۱۸ مگابایت) کپی شد · ۳ فایل در storage قدیم نبود |
| اجرای دوم و سوم | **صفر ردیف تکراری** |
### آنچه این ابزار عمداً منتقل نمی‌کند
خرده‌فروشی (`orders_orders`، `login`، `fav`، `wallets`، `transactions*` —
D13 و §۷.۲)، تاریخچه‌ی وضعیت سفارش (`order_activity`)، تخفیف‌ها
(`major_promotion*`)، اعلان‌ها، تاریخچه‌ی پیامک (`messages`)، لاگ‌ها،
نظرات، `cinies`. هرکدام لازم شد، قدم جدا اضافه می‌شود.
---
## ۱۱. جمع‌بندی — کارهای لازم قبل از نوشتن ابزار
### 🔴 مسدودکننده (بدون این‌ها ابزار در میانه می‌شکند یا داده خراب می‌کند)
@@ -1487,25 +1701,30 @@ legacy دکمه‌ی تکمیل پروفایل را فقط وقتی `sts_verify_
| ۱ | ~~شمارش تکرار~~ | ✅ **انجام شد** — ۲۷ گروه موبایل، ۰ کد ملی (§۱۰.۴) |
| ۱.۱ | ~~`nationalCode`~~ | ✅ **حل — nullable شود** (§۱۰.۴) |
| ۱.۲ | ~~`passwordHash`~~ | ✅ **حل — hash غیرقابل‌ورود** (§۱۰.۴) |
| ۱.۳ | 🔴 **مبلغ سند مالی کجاست؟** `cashes.remain` همه `NULL` | ۱۰.۶ |
| ۱.۳ | ~~مبلغ سند کجاست~~ | ✅ **حل — فرمول بدهی legacy با داده تأیید شد** (§۱۰.۷) |
| ۱.۴ | ~~`REJECTED`~~ | ✅ **حل — enum + دو ستون علت** (§۵.۵) |
| ۱.۵ | 🔴 **ترتیب دومرحله‌ای** — کارشناسان قبل از مشتریان | ۵.۷ |
| ۲ | آستانه‌ی تطبیق عددی جای «صفر اختلاف» — **تأیید شد: ۵۶ ردیف گِرد می‌شوند** | ۴ · ۱۰.۴ |
| ۳ | قاعده‌ی `WHERE deleted_at IS NULL` — **حیاتی‌تر از انتظار: ۴۵٪ واریانت‌ها** | ۵.۸ · ۱۰.۴ |
| ۱.۵ | ~~ترتیب دومرحله‌ای~~ | ✅ **در ابزار** — کارشناسان قبل از مشتریان (§۱۰.۸) |
| ۱.۶ | ~~`birth_date`~~ | ✅ **حل — پالایش، ۵۴۱ از ۹۱۵** (§۱۰.۷) |
| ۱.۷ | ~~`customers.type` ۳/۴/۵~~ | ✅ **سؤال لازم نبود** — در `Customer::$TYPES` تعریف شده‌اند (§۵.۳) |
| ۲ | ~~آستانه‌ی تطبیق~~ | ✅ **در ابزار** — ±۰٫۰۰۰۱ گرم، ±۱ ریال (§۱۰.۸) |
| ۳ | ~~قاعده‌ی `deleted_at`~~ | ✅ **در ابزار** — به‌جز حذف‌شده‌های ارجاع‌شده در سفارش (§۱۰.۷) |
| ۴ | ~~تصمیم `orders_orders`~~ | ✅ **حل — به تأخیر افتاد** (§۷.۲) |
| ۵ | تصحیح D0: `fname`/`lname` مستقیم، تجزیه نشود | ۲ |
| ۶ | **allow-list ستون‌های نوشتنی** — ۷ ستون مقصد-تنها لمس نشوند | **۱۰.۵** |
| ۷ | **کلید تطبیق ردیف‌های موجود مقصد** | **۱۰.۵** |
| ۵ | ~~تصحیح D0~~ | ✅ **در ابزار** — `fname`/`lname` مستقیم |
| ۶ | ~~allow-list ستون‌ها~~ | ✅ **در ابزار** — حسابداری بایت‌به‌بایت یکسان ماند (§۱۰.۸) |
| ۷ | ~~کلید تطبیق~~ | ✅ **موبایل** (مالک) — ادمین تنزل نمی‌یابد (§۱۰.۷) |
### 🔶 تصمیم مالک
| # | مورد | § |
|---|---|---|
| ۱ | ~~۴ ستون داده‌ی غیرقابل‌بازسازی~~ | ✅ **حل — تولد و شهر منتقل، توکن و سالگرد حذف** (§۵.۲) |
| ۱.۱ | 🔶 `new_city` → `cityId` یا ستون متنی؟ | **۵.۹** |
| ۱.۱ | ~~`new_city`~~ | ✅ **حل — نگاشت دوگانه: متن در `cityNameRaw` + `cityId` با تطابق دقیق** (مالک · §۱۰.۷) |
| ۲ | ~~تفکیک `STEP_TWO_SUBMITTED` از `VERIFIED`~~ | ✅ **حل** (§۵.۵) |
| ۳ | `type IS NULL` چه `CustomerKind` شود | ۵.۳ |
| ۴ | ~~`fav`~~ | ✅ **حل — لازم است، migration افزایشی** (§۱۰) |
| ۳ | ~~`type IS NULL`~~ | ✅ **`APP_SHOPKEEPER`** — ثبت‌نام اپ `type = 2` می‌گذارد (Major/CustomerController.php:44)؛ NULL قدیمی‌تر از آن است |
| ۴ | ~~علاقه‌مندی~~ | ✅ **حل — از `major_favorites`، نه `fav`** (§۱۰.۷) |
| ۴.۲ | ~~مشتری `#125`~~ | ✅ **همان −۹٫۸۹۵۱ با سند تراز انتقالی** (مالک · §۱۰.۷) |
| ۴.۳ | 🔶 **رد در پنل جدید `REJECTED` نمی‌نویسد** و مشتری رد‌شده با ارسال دوباره خودکار تأیید می‌شود | §۱۰.۵.۵ |
| ۴.۴ | 🔶 **مدیر ارشدی که کارشناس هم بود** — نقش ادمین نگه داشته شد؛ آیا حساب کارشناسی هم لازم است؟ | §۱۰.۷ |
| ۴.۱ | `cinies`/`ciny_products` — چیدمان نمایشی | ۱۰ |
| ۵ | شمارنده‌های denormalized محاسبه شوند یا صفر بمانند | ۵.۲ |
| ۶ | نسخه‌های تغییراندازه‌یافته‌ی تصویر | ۹ |
+4 -2
View File
@@ -23,7 +23,7 @@ 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 NOT NULL AND new_city <> '' AND city_id IS NOT NULL) AS both_set,
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;
@@ -32,7 +32,9 @@ FROM customers WHERE deleted_at IS NULL;
-- اگر بالا باشد، اکثرشان قابل تبدیل به 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)
-- The two columns have different collations (unicode_ci vs general_ci), so
-- both sides are pinned to one; without it MySQL refuses the comparison.
JOIN cities ci ON TRIM(ci.title) COLLATE utf8mb4_general_ci = TRIM(c.new_city) COLLATE utf8mb4_general_ci
WHERE c.deleted_at IS NULL AND c.new_city IS NOT NULL AND c.new_city <> ''
AND c.city_id IS NULL;