diff --git a/docs/DATA-MIGRATION-MAP.md b/docs/DATA-MIGRATION-MAP.md index 3a8822c..e0ead25 100644 --- a/docs/DATA-MIGRATION-MAP.md +++ b/docs/DATA-MIGRATION-MAP.md @@ -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 "" \ + --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 محاسبه شوند یا صفر بمانند | ۵.۲ | | ۶ | نسخه‌های تغییراندازه‌یافته‌ی تصویر | ۹ | diff --git a/docs/migration-precheck-3.sql b/docs/migration-precheck-3.sql index 192c727..e035302 100644 --- a/docs/migration-precheck-3.sql +++ b/docs/migration-precheck-3.sql @@ -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;