- Add Tahesab/Zarrin accounting sync module with admin panel UI - Add SMS relay driver as default provider, refactor SMS into driver pattern - Update customer services, orders, products, and catalog queries - Rebuild user-app and user-app-v2 dist assets - Add Prisma migration for accounting tables - Update docker-compose and env examples - Update parity docs (DECISIONS, ISSUES, PROGRESS) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1155 lines
61 KiB
Markdown
1155 lines
61 KiB
Markdown
# نگاشت دیتابیس legacy → Prisma
|
||
|
||
> سند **نگاشت**، نه پلن اجرا. هیچ کدی بر اساس این سند نوشته نشده.
|
||
> پیشنیاز ابزار مهاجرت دادهی [`GO-LIVE-PLAN.md`](./GO-LIVE-PLAN.md) §۷.
|
||
|
||
**منبع:** `oapi-struncture.sql` — دامپ Navicat، تاریخ ۰۵/۱۰/۲۰۲۶
|
||
**مقصد:** `apps/api/prisma/schema.prisma` — ۴۳ مدل، ۴۷۷ فیلد
|
||
**تاریخ نگارش:** ۱۴ مهر ۱۴۰۵ (۲۰۲۶-۱۰-۰۶)
|
||
|
||
---
|
||
|
||
## ۰. مشخصات منبع
|
||
|
||
| ویژگی | مقدار |
|
||
|---|---|
|
||
| موتور | **MySQL 8.0.42** (`8.0.42-0ubuntu0.20.04.1`) |
|
||
| سرور | `Henza` · `localhost:3305` |
|
||
| اسکیما | `oapi` |
|
||
| جداول | **۸۰** |
|
||
| ستونها | **۱٬۰۸۷** |
|
||
| Engine | `InnoDB` (هر ۸۰ جدول) |
|
||
| محتوا | **فقط ساختار — صفر `INSERT`** |
|
||
|
||
این فایل شکافی را پر کرد که قبلاً باز بود: جداول دوران Django
|
||
(`product`، `products_productvarient`، `category_collection` و …) هیچ migration
|
||
لاراولی نداشتند و فقط در دیتابیس زنده تعریف شده بودند. **موتور دیتابیس هم تا
|
||
پیش از این حدس بود؛ اکنون قطعی است: MySQL.**
|
||
|
||
⚠️ **دامپ داده ندارد.** پس این سند نگاشت *ساختار* است. آستانههای تطبیق §۷
|
||
`GO-LIVE` و شمارش ردیفهای تکراری (§۳ همین سند) **دادهی واقعی لازم دارند** و
|
||
با این فایل قابل انجام نیستند.
|
||
|
||
---
|
||
|
||
## ۱. تفکیک ۸۰ جدول → ۵۰ زنده، ۳۰ مرده
|
||
|
||
«زنده» = مدل Eloquent دارد (`$table` صریح، یا نام جمعِ پیشفرض لاراول).
|
||
|
||
| دسته | تعداد | تصمیم |
|
||
|---|---|---|
|
||
| **زنده — در دامنهی کار** | **۴۱** | مهاجرت میشوند |
|
||
| زنده — فروشگاه خرده (D13) | ۹ | **حفظ، بدون پورت** |
|
||
| نسخهی پشتیبان دستی | ۶ | نادیده |
|
||
| بازماندهی Django | ۱۱ | نادیده |
|
||
| زیرساخت لاراول | ۳ | نادیده |
|
||
|
||
### ۱.۱ — جداول پشتیبان دستی (۶) — نادیده
|
||
|
||
```
|
||
customers_copy1 customers_copy2 products_productvarient_copy1
|
||
cities5 citiesB cities_copy11
|
||
```
|
||
|
||
`customers_copy1` و `customers_copy2` هر دو ۴۹ ستون دارند، **برابر با
|
||
`customers`**. snapshot دستی قبل از یک تغییرند. `customers_test` (۴۰ ستون) مدل
|
||
`CustomerTest` دارد ولی جدول آزمایشی است.
|
||
|
||
⚠️ **قبل از نادیدهگرفتن، تعداد ردیف هر سه `customers*` شمرده شود.** اگر
|
||
`copy2` ردیفهایی دارد که `customers` ندارد، یعنی جایی ردیف حذف شده و این کپی
|
||
آخرین نسخهی آن است.
|
||
|
||
### ۱.۲ — بازماندهی Django (۱۱) — نادیده
|
||
|
||
```
|
||
accounts_effectivedate accounts_profile accounts_rolepermission auth_permission
|
||
discount_discountcode discount_pricerule
|
||
orders_fulfillment orders_sendtime
|
||
products_comment products_promotion products_variantpromotion
|
||
```
|
||
|
||
هیچکدام مدل Eloquent ندارند. `accounts_profile` با ۴۳ ستون بزرگترینشان است
|
||
(`sales_ceiling`، `commission`، `profit`) — سامانهی کارشناس فروشِ نسخهی
|
||
Django، که لاراول با `customers.type=1` جایش را گرفت.
|
||
|
||
⚠️ `orders_sendtime` از `orders_orders.send_time_id` **ارجاع میشود**. اگر
|
||
زمان ارسال سفارشهای تاریخی مهم است، این جدول مرده نیست.
|
||
|
||
### ۱.۳ — فروشگاه خرده (۹) — حفظ، بدون پورت (D13)
|
||
|
||
```
|
||
wallets transactions transaction_metas transactions_cheque
|
||
transactions_deposittocard transactions_financialreport
|
||
transactions_meltedgold transactions_refund discount_codes
|
||
```
|
||
|
||
طبق D13 نگه داشته میشوند. `TransactionOrder` و `transaction_orders` مدل
|
||
دارند، ولی `transactions_*` ندارند.
|
||
|
||
---
|
||
|
||
## ۲. 🔴 `fname`/`lname` از قبل وجود دارند — D0 بر فرض غلط بنا شد
|
||
|
||
جدول `customers` **هر سه ستون** را دارد:
|
||
|
||
```
|
||
fname varchar(50) NULL
|
||
lname varchar(50) NULL
|
||
name varchar(75) NULL
|
||
```
|
||
|
||
و `users` هم همین سه را دارد (`fname`, `lname`, `name` — هر سه `varchar(255)`).
|
||
|
||
`DECISIONS.md` §D0 ثبت کرده که `fullName` باید «به `firstName`/`lastName`
|
||
شکسته شود» با backfill و «گزارش ردیفهای چندتوکنی». **چیزی برای شکستن نیست.**
|
||
legacy از اول جدا داشته.
|
||
|
||
| نگاشت صحیح | نگاشت غلط |
|
||
|---|---|
|
||
| `fname → firstName` | ~~تجزیهی `name` با توکن اول/بقیه~~ |
|
||
| `lname → lastName` | |
|
||
| `name → fullName` | |
|
||
|
||
**اگر از `name` تجزیه کنیم، دادهی درست را با حدس خراب میکنیم.**
|
||
|
||
### تلهی trigger
|
||
|
||
`customer_sync_full_name` در Postgres یک trigger است و طبق
|
||
`PROGRESS.md` رفتارش این است: **اگر هر سه را یکجا بنویسی، اجزا برنده میشوند
|
||
و `fullName` ارسالی دور ریخته میشود.** برای مهاجرت این دقیقاً رفتار مطلوب
|
||
است (اجزا منبع حقیقتاند) — ولی اگر `name` در legacy با `fname + ' ' + lname`
|
||
نخواند، **مقدار `name` در مهاجرت گم میشود**.
|
||
|
||
⚠️ قبل از مهاجرت شمرده شود: `WHERE name != CONCAT(fname,' ',lname)`. اگر
|
||
صفر نیست، تصمیم مالک لازم است کدام برنده باشد.
|
||
|
||
---
|
||
|
||
## ۳. 🔴 هیچ `UNIQUE` روی `mobile` و `ncode` نیست
|
||
|
||
کل ایندکسهای `customers`:
|
||
|
||
```sql
|
||
PRIMARY KEY (`id`) USING BTREE
|
||
```
|
||
|
||
همین. **هیچ unique index دیگری، و هیچ foreign key.**
|
||
|
||
ولی Prisma اعلام کرده:
|
||
|
||
```prisma
|
||
mobile String @unique
|
||
nationalCode String @unique
|
||
```
|
||
|
||
**نتیجه:** به احتمال زیاد در دادهی تولید موبایل یا کد ملی تکراری وجود دارد.
|
||
مهاجرت در میانه با خطای unique متوقف میشود — و چون در میانه است، نیمی از
|
||
داده منتقل شده.
|
||
|
||
`GO-LIVE §۷` تکرار شماره **بین `Customer` و `AdminUser`** را ثبت کرده، ولی
|
||
**تکرار داخل خود `customers` را ندیده**. این یک شکاف تازه است.
|
||
|
||
### کوئریهای شمارش — در فایل جداگانه
|
||
|
||
📄 **[`migration-precheck.sql`](./migration-precheck.sql)** — ۹ بخش،
|
||
فقط `COUNT`/`SUM`، صفر `INSERT`/`UPDATE`/`DELETE`. هیچ ستون متنی در
|
||
خروجی ظاهر نمیشود، پس **دادهی مشتری از سرور بیرون نمیآید**.
|
||
|
||
```
|
||
mysql -u USER -p -h HOST -P 3305 oapi < migration-precheck.sql > out.txt
|
||
```
|
||
|
||
نمونهی مختصر از بخش ۲ همان فایل:
|
||
|
||
```sql
|
||
-- ۱) موبایل تکراری
|
||
SELECT mobile, COUNT(*) c FROM customers
|
||
WHERE deleted_at IS NULL GROUP BY mobile HAVING c > 1;
|
||
|
||
-- ۲) کد ملی تکراری (رشتهی خالی هم تکرار است، برخلاف NULL)
|
||
SELECT ncode, COUNT(*) c FROM customers
|
||
WHERE deleted_at IS NULL AND ncode IS NOT NULL AND ncode != ''
|
||
GROUP BY ncode HAVING c > 1;
|
||
|
||
-- ۳) موبایل یا کد ملی تهی — Prisma هر دو را NOT NULL میخواهد
|
||
SELECT
|
||
SUM(mobile IS NULL OR mobile = '') AS no_mobile,
|
||
SUM(ncode IS NULL OR ncode = '') AS no_ncode
|
||
FROM customers WHERE deleted_at IS NULL;
|
||
|
||
-- ۴) تداخل شماره با کارشناسان و ادمینها (D21: «کارشناس برنده است»)
|
||
SELECT c.mobile FROM customers c
|
||
JOIN users u ON u.mobile = c.mobile
|
||
WHERE c.deleted_at IS NULL AND u.deleted_at IS NULL;
|
||
```
|
||
|
||
`nationalCode` در Prisma `String` بدون `?` است — **اجباری**. اگر کوئری ۳
|
||
عددی غیرصفر برگرداند، یا schema باید nullable شود، یا مالک باید قاعدهی
|
||
جایگزین بدهد.
|
||
|
||
---
|
||
|
||
## ۴. 🔴 ۱۱۰ ستون پول و وزن `double` هستند — صفر `decimal`
|
||
|
||
```
|
||
double/float : ۱۱۰ ستون
|
||
decimal : ۱ ستون (فقط users.permission — که مرده است، §۶)
|
||
```
|
||
|
||
نمونهها: `customers.remain`, `customers.rial_remain`, `cashes.remain`,
|
||
`cashes.balance`, `orders.w`, `orders.wage`, `order_items.rash_weight`,
|
||
`products_productvarient.grams`.
|
||
|
||
Prisma درستکاری کرده: `Decimal(14,4)` برای گرم، `Decimal(20,2)` برای ریال.
|
||
|
||
**ولی `GO-LIVE §۷` شرط تطبیق را «صفر اختلاف» گذاشته — و این ذاتاً
|
||
دستنیافتنی است.**
|
||
|
||
`double` در IEEE 754 عدد `12.3` را واقعاً `12.3000000000000007105…`
|
||
ذخیره میکند. تبدیل به `Decimal(14,4)` آن را به `12.3000` گِرد میکند. در یک
|
||
ردیف بیاهمیت است؛ در جمعِ هزاران ردیفِ دفتر، اختلاف انباشته میشود.
|
||
|
||
### پیشنهاد — آستانهی واقعبینانه جای صفر مطلق
|
||
|
||
| بررسی | آستانهی فعلی §۷ | آستانهی پیشنهادی |
|
||
|---|---|---|
|
||
| بدهی طلای هر مشتری | صفر اختلاف | `|Δ| < 0.0001` گرم |
|
||
| بدهی ریالی هر مشتری | صفر اختلاف | `|Δ| < 1` ریال |
|
||
| جمع کل بدهی طلا | — | `|Δ| < 0.01` گرم |
|
||
|
||
بدون این، ابزار مهاجرت **همیشه قرمز** گزارش میدهد و کسی به گزارشش اعتماد
|
||
نمیکند — که بدتر از نداشتنِ گزارش است.
|
||
|
||
⚠️ `rial_*` در legacy `bigint` است (نه `double`) — یعنی **ریال صحیح، بدون
|
||
اعشار**. ولی Prisma `Decimal(20,2)` گرفته. این سمت مشکلی ندارد (دقت بیشتر)،
|
||
ولی تأیید میکند که `fiatDebtRial` هرگز اعشار واقعی ندارد. نقص #۱۹ در
|
||
`PWA-PORT-PLAN.md` («بررسی واحد») اینجا پاسخ میگیرد: **واحد ریال است، صحیح.**
|
||
|
||
---
|
||
|
||
## ۵. `customers` — نگاشت ۴۹ ستون
|
||
|
||
۳۲ ستون نگاشت دارند، **۱۷ ستون خانه ندارند**.
|
||
|
||
### ۵.۱ — نگاشتشده (۳۲)
|
||
|
||
| legacy | نوع | Prisma | یادداشت |
|
||
|---|---|---|---|
|
||
| `id` | `bigint` | — | در جدول نگاشت `legacyId` نگه داشته شود (§۷ GO-LIVE) |
|
||
| `fname` | `varchar(50)` | `firstName` | §۲ — مستقیم، تجزیه نشود |
|
||
| `lname` | `varchar(50)` | `lastName` | §۲ |
|
||
| `name` | `varchar(75)` | `fullName` | trigger بازنویسی میکند — §۲ |
|
||
| `mobile` | `varchar(20)` | `mobile` | **§۳ — unique نیست در مبدأ** |
|
||
| `ncode` | `varchar(20)` | `nationalCode` | **§۳ — unique نیست، و در Prisma اجباری است** |
|
||
| `password` | `varchar(255)` | `passwordHash` | فرمت hash تأیید شود (bcrypt؟) |
|
||
| `city_id` | `int` | `cityId` | نگاشت id لازم |
|
||
| `type` | `tinyint` | `kind` | §۵.۳ |
|
||
| `sts` | `tinyint` | `canPlaceOrders` | `1`→`true`, `-1`→`false` |
|
||
| `active` | `tinyint` | `isActive` | سوییچ مجزا از `sts` |
|
||
| `is_archive` | `tinyint(1)` | `isArchived` | |
|
||
| `gram_level` | `double` | `goldCreditLimitGrams` | §۴ |
|
||
| `remain` | `double` | `goldDebtGrams` | §۴ — بدهی طلا |
|
||
| `rial_remain` | `bigint` | `fiatDebtRial` | §۴ — ریال صحیح |
|
||
| `percent` | `double` | `makingChargeOffsetPercent` | D9 — آفست اجرت |
|
||
| `reagent_id` | `int` | `referredByAgentId` | 🔴 **→ `customers.id`، نه `users.id`** · §۵.۷ |
|
||
| `phone` | `varchar(15)` | `phone` | |
|
||
| `address` | `varchar(255)` | `address` | |
|
||
| `store_name` | `varchar(255)` | `storeName` | |
|
||
| `pasazh_name` | `varchar(255)` | `pasazhName` | |
|
||
| `zone` | `varchar(255)` | `zone` | |
|
||
| `mcode` | `varchar(30)` | `memberCode` | |
|
||
| `image_id` | `int` | `avatarFileId` | FK → `UploadedFile` |
|
||
| `ncode_image_id` | `int` | `nationalIdImageUrl` | **FK → رشته** · §۵.۴ |
|
||
| `business_license_image_id` | `int` | `businessLicenseUrl` | **FK → رشته** · §۵.۴ |
|
||
| `shop_image_id` | `int` | `shopImageUrl` | **FK → رشته** · §۵.۴ |
|
||
| `sts_verify_step_one` | `tinyint(1)` | `verificationStep` | §۵.۵ — دو ستون → یک enum |
|
||
| `sts_verify_step_two` | `tinyint(1)` | `verificationStep` | §۵.۵ |
|
||
| `created_at` | `timestamp` | `createdAt` | |
|
||
| `updated_at` | `timestamp` | `updatedAt` | |
|
||
| `deleted_at` | `timestamp` | — | **§۵.۶ — soft-delete در Prisma وجود ندارد** |
|
||
|
||
### ۵.۲ — 🟡 ۱۷ ستون بیخانه
|
||
|
||
**هر ۱۷ مورد در کد legacy فعالاً استفاده میشوند** (با grep تأیید شد).
|
||
اگر ابزار مهاجرت بیخبر نوشته شود، **بیصدا دور ریخته میشوند**.
|
||
|
||
| ستون | نوع | فایلهای استفادهکننده | چیست |
|
||
|---|---|---|---|
|
||
| `input` | `double` | — | جمع واردهی طلا |
|
||
| `output` | `double` | — | جمع صادرهی طلا |
|
||
| `rial_input` | `bigint` | — | جمع واردهی ریال |
|
||
| `rial_output` | `bigint` | — | جمع صادرهی ریال |
|
||
| `grams_sell` | `double` | ۳ | گرم فروختهشده |
|
||
| `total_order` | `int` | ۶ | شمارندهی سفارش |
|
||
| `open_order` | `int` | ۵ | شمارندهی سفارش باز |
|
||
| `child` | `int` | ? | شمارندهی زیرمجموعه |
|
||
| `notify_count` | `int` | ۵ | شمارندهی اعلان خواندهنشده |
|
||
| `count_login` | `bigint` | ۲ | شمارندهی ورود |
|
||
| `token_mobile` | `varchar(255)` | **۱۰** | توکن push گوشی |
|
||
| `birth_date` | `timestamp` | ۴ | تولد — منطق تبریک |
|
||
| `marriage_date` | `timestamp` | ۳ | سالگرد |
|
||
| `new_city` | `varchar(255)` | ۴ | شهر دستی وقتی `city_id` تهی است |
|
||
| `api_token` | `varchar(60)` | — | auth قدیم |
|
||
| `send_password` | `varchar(255)` | ۴ | رمز ارسالی با پیامک |
|
||
| `rnd` | `varchar(60)` | ۴ | nonce |
|
||
|
||
**سه گروه با تصمیم متفاوت:**
|
||
|
||
**الف) `input`/`output`/`rial_input`/`rial_output` — طرف دیگر دفتر.**
|
||
Prisma فقط `remain` را بهعنوان ستون دارد. در مدل جدید جمع وارده/صادره باید
|
||
از `LedgerEntry` محاسبه شود. **این بهتر است** — ولی یعنی هنگام مهاجرت باید
|
||
تأیید شود که `SUM(ledger) == remain` legacy. اگر نخواند، legacy خودش ناسازگار
|
||
بوده و باید گزارش شود، نه اصلاح.
|
||
|
||
**ب) شمارندههای denormalized** (`total_order`, `open_order`, `notify_count`,
|
||
`child`, `count_login`, `grams_sell`). قابل محاسبه از دادهی دیگرند. اضافه
|
||
نکردنشان درست است، **به شرطی که جایی که legacy نشانشان میداد، مقصد هم
|
||
محاسبهشان کند** — وگرنه عدد صفر نشان داده میشود.
|
||
|
||
**ج) دادهی واقعیِ غیرقابلبازسازی** — `birth_date`, `marriage_date`,
|
||
`token_mobile`, `new_city`. **اینها اگر منتقل نشوند، برای همیشه از بین
|
||
میروند.** `token_mobile` در ۱۰ فایل استفاده میشود؛ بدون آن، اعلان push به
|
||
گوشیهای موجود قطع میشود.
|
||
|
||
🔶 **تصمیم مالک لازم است برای گروه (ج).**
|
||
|
||
`api_token`, `send_password`, `rnd` نیازی نیست منتقل شوند — سامانهی auth
|
||
جدید JWT است.
|
||
|
||
### ۵.۳ — `type` → `CustomerKind`
|
||
|
||
`Customer::$TYPES` در legacy پنج مقدار دارد:
|
||
|
||
| legacy | برچسب | `CustomerKind` |
|
||
|---|---|---|
|
||
| `1` | کارشناس فروش | **→ `AdminUser` + `SalesExpertProfile`** (نه `Customer`) · D17 |
|
||
| `2` | مغازهدار اپلیکیشن | `APP_SHOPKEEPER` |
|
||
| `3` | مغازهدار حضوری | `WALK_IN_SHOPKEEPER` |
|
||
| `4` | بنکدار | `BANKER` |
|
||
| `5` | سرپرست کارگاه | `WORKSHOP_SUPERVISOR` |
|
||
| — | — | `RETAIL` ➕ مقصد-تنها (از `login`، §۸) |
|
||
| `NULL` | — | 🔶 **بینگاشت** |
|
||
|
||
`type` در مبدأ **nullable بدون default** است. Prisma
|
||
`@default(APP_SHOPKEEPER)` دارد. پس `NULL` بیصدا «مغازهدار اپلیکیشن»
|
||
میشود. **قبل از مهاجرت `SELECT COUNT(*) WHERE type IS NULL` شمرده شود.**
|
||
|
||
⚠️ `type=1` **به جدول دیگری میرود** (`AdminUser`). یعنی مهاجرت `customers`
|
||
دوشاخه است و جدول نگاشت باید بداند هر `legacyId` به کدام جدول رفته.
|
||
|
||
### ۵.۴ — سه FK تصویر → سه رشته
|
||
|
||
| legacy | نوع | Prisma | نوع |
|
||
|---|---|---|---|
|
||
| `ncode_image_id` | `int` → `image.id` | `nationalIdImageUrl` | `String?` |
|
||
| `business_license_image_id` | `int` → `image.id` | `businessLicenseUrl` | `String?` |
|
||
| `shop_image_id` | `int` → `image.id` | `shopImageUrl` | `String?` |
|
||
|
||
در legacy اینها **FK** به جدول `image` بودند. در Prisma **رشتهی URL** شدند.
|
||
پس مهاجرت باید `image.image` (مسیر فایل، `char(100)`) را بخواند و URL بسازد.
|
||
|
||
⚠️ `image_id` (آواتار) **متفاوت** رفتار میکند: در Prisma هنوز FK است
|
||
(`avatarFileId → UploadedFile`). یعنی **چهار ستون تصویر، دو الگوی مختلف.**
|
||
این ناسازگاری در مقصد است، ولی عوضکردنش الان کار این سند نیست — فقط ثبت
|
||
میشود.
|
||
|
||
**فایلهای تصویر هم باید کپی شوند** (§۷ GO-LIVE) — جدول `image` فقط مسیر دارد.
|
||
|
||
### ۵.۵ — 🔴 دو ستون تأیید **boolean نیستند** — سهحالتهاند
|
||
|
||
**تصحیح ۲۰۲۶-۱۰-۰۶:** نسخهی اول این سند فرض کرده بود این دو `tinyint(1)`
|
||
مقدار `0`/`1` دارند. **غلط بود.** دادهی واقعی و
|
||
`MajorCustomerController.php:106-135` نشان میدهند مقادیر `1`, `2`, `3`
|
||
هستند:
|
||
|
||
| مقدار | معنا | عارضه در legacy |
|
||
|---|---|---|
|
||
| `1` | در انتظار بررسی | — |
|
||
| `2` | **تأیید شد** | `step_one=2` → `active=1` + `api_token` + پیامک |
|
||
| `3` | **رد شد** | `step_one=3` → `active=-1` · `step_two=3` → پیامک رد |
|
||
| `NULL` | هنوز وارد نشده | — |
|
||
|
||
### توزیع واقعی (۱٬۵۹۵ ردیف زنده)
|
||
|
||
| `one,two` | تعداد | معنا | `VerificationStep` |
|
||
|---|---|---|---|
|
||
| `2,2` | **۱٬۴۶۵** | هر دو تأیید | `VERIFIED` |
|
||
| `1,NULL` | ۹۰ | گام ۱ در انتظار | `STEP_ONE_SUBMITTED` |
|
||
| `2,NULL` | ۲۵ | گام ۱ تأیید، گام ۲ وارد نشده | `STEP_ONE_SUBMITTED` |
|
||
| `1,2` | ۷ | 🔴 **ناسازگار** — گام ۲ تأیید ولی گام ۱ در انتظار | ؟ |
|
||
| `3,2` | ۳ | 🔴 **ناسازگار** — گام ۱ رد ولی گام ۲ تأیید | ؟ |
|
||
| `2,1` | ۱ | گام ۲ در انتظار | `STEP_TWO_SUBMITTED` |
|
||
| `2,3` | ۲ | گام ۲ رد | ؟ |
|
||
| `3,3` | ۲ | هر دو رد | ؟ |
|
||
|
||
🔴 **`VerificationStep` حالت «رد شد» ندارد.** مقادیرش
|
||
`UNVERIFIED` / `STEP_ONE_SUBMITTED` / `STEP_TWO_SUBMITTED` / `VERIFIED`
|
||
است — هیچ `REJECTED`. پس **۷ ردیف ردشده** (`2,3` + `3,3` + بخشی از `3,2`)
|
||
جایی برای رفتن ندارند و اگر `UNVERIFIED` شوند، **از «ردشده» به
|
||
«بررسینشده» برمیگردند** — یعنی دوباره در صف بررسی ظاهر میشوند.
|
||
|
||
🔶 **تصمیم مالک — دو گزینه:**
|
||
1. افزودن `REJECTED` به enum (additive، با §G تضاد ندارد) — ۱۴ ردیف درست
|
||
منتقل میشوند
|
||
2. نگاشت ردشدهها به `UNVERIFIED` و پذیرفتن اینکه در صف برمیگردند
|
||
|
||
و **۱۰ ردیف ناسازگار** (`1,2` و `3,2`) هر تصمیمی بگیرید، نیاز به قاعدهی
|
||
صریح دارند: گام دوم تأیید شده ولی گام اول نه. در legacy این حالت ممکن بود
|
||
چون دو ستون مستقلاند و هیچ محدودیتی بینشان نیست.
|
||
|
||
### ۵.۷ — 🔴 `reagent_id` به `customers` اشاره میکند، نه `users`
|
||
|
||
کوئری پیشبررسی **۱٬۵۳۴ ارجاع یتیم** از `customers.reagent_id` به
|
||
`users.id` گزارش کرد — یعنی تقریباً همهی ۱٬۵۹۵ ردیف.
|
||
|
||
**علت: کوئری من به جدول غلط join زده بود، نه اینکه داده خراب باشد.**
|
||
|
||
`Customer.php:119-122`:
|
||
```php
|
||
public function reagent()
|
||
{
|
||
return $this->belongsTo('App\Models\Customer'); // ← Customer، نه User
|
||
}
|
||
```
|
||
|
||
کارشناس فروش در legacy **یک ردیف در `customers` با `type=1`** است (۵۶ ردیف،
|
||
§۵.۳) — نه یک ردیف در `users`. پس `reagent_id` یک **خودارجاع** است.
|
||
|
||
### پس در مقصد
|
||
|
||
`Customer.referredByAgentId` به **`AdminUser`** اشاره میکند، چون D17 حکم
|
||
کرده کارشناس فروش `AdminUser` + `SalesExpertProfile` شود. یعنی مهاجرت باید
|
||
**دو مرحلهای** باشد:
|
||
|
||
1. اول ۵۶ ردیف `customers.type=1` → `AdminUser` + `SalesExpertProfile`،
|
||
و `legacyCustomerId → adminUserId` در جدول نگاشت ثبت شود
|
||
2. بعد بقیهی مشتریها، و `reagent_id` از **همان جدول نگاشت** ترجمه شود —
|
||
نه از `users`
|
||
|
||
🔴 **اگر این ترتیب رعایت نشود، `referredByAgentId` همهی مشتریها `NULL`
|
||
میشود** و رابطهی «کارشناس معرف» کامل از بین میرود.
|
||
|
||
⚠️ `mobile_collision_customer_vs_user = 10` یعنی ۱۰ شماره در هر دو جدول
|
||
هست. چون کارشناس در `customers` است، این ۱۰ مورد **تداخل واقعی**اند و
|
||
D21 («کارشناس برنده است») دقیقاً برایشان نوشته شده.
|
||
|
||
---
|
||
|
||
### ۵.۸ — 🟡 `deleted_at` در Prisma وجود ندارد
|
||
|
||
هر جدول زندهی legacy ستون `deleted_at` دارد (soft-delete لاراول).
|
||
**هیچ مدل Prisma معادلی ندارد.**
|
||
|
||
یعنی ردیفهای حذفشدهی نرم:
|
||
- یا منتقل نمیشوند (**پیشنهاد** — `WHERE deleted_at IS NULL`)
|
||
- یا منتقل میشوند و **ناگهان زنده میشوند** 🔴
|
||
|
||
گزینهی دوم فاجعه است: مشتری حذفشده برمیگردد، سفارش حذفشده در گزارش
|
||
ظاهر میشود، سند مالی حذفشده در موجودی حساب میشود.
|
||
|
||
**قاعدهی اجباری ابزار مهاجرت: هر `SELECT` باید `WHERE deleted_at IS NULL`
|
||
داشته باشد.** و تعداد ردیفهای حذفشدهی هر جدول در گزارش تطبیق بیاید تا
|
||
معلوم باشد چه حجمی عمداً جا مانده.
|
||
|
||
---
|
||
|
||
## ۶. `users` → `AdminUser` — و یک ستون خراب
|
||
|
||
| legacy | نوع | Prisma | یادداشت |
|
||
|---|---|---|---|
|
||
| `fname`/`lname`/`name` | `varchar(255)` | `firstName`/`lastName` | D17 |
|
||
| `mobile` | `varchar(255)` | — | بررسی تداخل با `customers` (§۳) |
|
||
| `password` | `varchar(255)` | `passwordHash` | |
|
||
| `ut` | `int unsigned` | `userTypeId` → `UserType` | کلید ACL |
|
||
| `city_id`, `branch_id` | `int` | — | نگاشت id |
|
||
| `token_mobile`, `notify_count` | | 🟡 بیخانه | مثل §۵.۲ |
|
||
| `api_token`, `remember_token` | | — | لازم نیست |
|
||
| **`permission`** | **`decimal(16,16)`** | — | **🔴 ستون خراب — زیر** |
|
||
|
||
### 🔴 `users.permission decimal(16,16)` — ستونی که کار نمیکند
|
||
|
||
`decimal(16,16)` یعنی **۱۶ رقم کل، ۱۶ رقم اعشار** — پس **صفر رقم صحیح**.
|
||
این ستون فقط میتواند مقداری بین `0.0000…` و `0.9999…` نگه دارد. هر عدد
|
||
صحیحی مثل `1` در آن **سرریز** میکند.
|
||
|
||
خوشبختانه **مرده است**: ACL واقعی از جدول `permission(rid, ut)` میخواند —
|
||
`AdminController.php:74` → `Permission::whereUt($user->ut)->get()`.
|
||
|
||
**نگاشت: نادیده.** ثبت میشود چون نشان میدهد اسکیمای legacy در جاهایی
|
||
بازماندهی بیاستفاده دارد، و اگر کسی بعداً این ستون را ببیند فکر میکند
|
||
دادهی معنیدار است.
|
||
|
||
### `permission` → `Permission` + `UserType`
|
||
|
||
```
|
||
permission (rid varchar(255), ut int) -- ۲ ستون، بدون PK
|
||
```
|
||
|
||
جدول دوستونیِ خالصِ نگاشت. ۱۴۴ rid در مقصد (`acl.constants.ts`) پوشش داده
|
||
شدهاند. **بدون primary key** — پس ردیف تکراری ممکن است؛ هنگام مهاجرت
|
||
`DISTINCT` شود.
|
||
|
||
---
|
||
|
||
## ۷. سفارشها — دو جدول جدا → یک مدل
|
||
|
||
legacy **دو جدول کاملاً مستقل** دارد، و هیچکدام فیلتر نوع ندارند چون خودِ
|
||
جدول تفکیککننده است:
|
||
|
||
| جدول | ستون | نقش | `OrderChannel` |
|
||
|---|---|---|---|
|
||
| `orders` | ۱۹ | عمده (B2B) | `WHOLESALE` |
|
||
| `orders_orders` | ۳۷ | خرده (Django-era) | `RETAIL` |
|
||
|
||
### ۷.۱ — `orders` → `Order` (عمده)
|
||
|
||
| legacy | نوع | Prisma | یادداشت |
|
||
|---|---|---|---|
|
||
| `customer_id` | `int` | `customerId` | نگاشت id |
|
||
| `reagent_id` | `int` | — | کارشناس سفارش |
|
||
| `type` | `tinyint` | — | 🔶 نوع ثبت (تلفنی/تلگرام/…) |
|
||
| `w` | `double` | وزن | §۴ |
|
||
| `wage` | `double` | اجرت | §۴ |
|
||
| `percent` | `double` | درصد اجرت | §۴ · معنای سوم `percent` |
|
||
| `sts` | `tinyint` | `OrderStatus` | §۷.۳ |
|
||
| `confirmed` | `tinyint(1)` | — | 🔶 جدا از `sts` |
|
||
| `input`/`output`/`remain` | `double` | — | طرف دفتر |
|
||
| `price`, `rial_input`, `discount` | | | §۴ |
|
||
| `city_id` | `int` | — | |
|
||
| `deleted_at` | `timestamp` | — | **§۵.۶** |
|
||
|
||
### ۷.۲ — `orders_orders` → `Order` با `channel=RETAIL`
|
||
|
||
۳۷ ستون، شامل `billing_address`, `shipping_address`, `browser_ip`,
|
||
`buyer_accepts_marketing`, `email`, `total_tax`, `use_wallet`,
|
||
`payment_after_wallet`, `gold_price`, `gold_price_original`.
|
||
|
||
طبق **D13 فروشگاه خرده خارج از دامنه است** — پس این جدول **حفظ میشود،
|
||
پورت نمیشود**. ولی `Order.channel` مقدار `RETAIL` دارد، یعنی مقصد جای
|
||
نگهداشتنش را دارد.
|
||
|
||
### ✅ تصمیم مالک ۲۰۲۶-۱۰-۰۶ — به تأخیر افتاد
|
||
|
||
> «الان مهم نیست و در زمان مناسب بهش ورود میکنیم برای تکفروشی.»
|
||
|
||
**`orders_orders` در این مرحله منتقل نمیشود.** نتایج:
|
||
|
||
- 🟢 **خطر تعارض `sts=7` فعلاً منتفی است** (§۷.۳). سفارش تحویلشده به
|
||
کنسلشده تبدیل نمیشود، چون هیچ ردیفی منتقل نمیشود.
|
||
- 🟢 `orders_orderitem` (۲۹ ستون) هم منتقل نمیشود.
|
||
- 🟢 `orders_sendtime` و `orders_fulfillment` هم بیاثر میشوند.
|
||
- 🟡 `OrderChannel.RETAIL` خالی میماند — انتظارش همین است، باگ نیست.
|
||
- 🔴 **جدول در MySQL دستنخورده نگه داشته شود.** وقتی تکفروشی وارد کار شد،
|
||
منبع همین جدول است. هیچ `DROP` و `TRUNCATE` روی `orders_orders`،
|
||
`orders_orderitem`، `orders_sendtime`.
|
||
|
||
⚠️ **بدهی فنیِ ثبتشده:** هر وقت تکفروشی وارد شود، **جدول نگاشت وضعیتِ
|
||
دومی** لازم است چون `sts` در دو جدول معانی متضاد دارد (§۷.۳). این
|
||
نمیتواند با همان نگاشت عمده انجام شود.
|
||
|
||
### ۷.۳ — 🔴 دو enum وضعیت متضاد (MON-07 در سطح داده)
|
||
|
||
`Order::$STS` (عمده):
|
||
|
||
| # | برچسب | `OrderStatus` |
|
||
|---|---|---|
|
||
| `0` | معلق | `SUSPENDED` |
|
||
| `1` | ثبت سفارش | `PLACED` |
|
||
| `2` | تایید مدیر سیستم | `ADMIN_APPROVED` |
|
||
| `3` | درحال تامین | `SOURCING` |
|
||
| `4` | درحال آماده سازی | `PREPARING` |
|
||
| `5` | تحویل مسئول فروش | `DELIVERED_TO_AGENT` |
|
||
| `6` | تحویل مشتری | `DELIVERED_TO_CUSTOMER` |
|
||
| `7` | ~~کنسل شده~~ (کامنت) | `CANCELLED` |
|
||
|
||
`SingleCustomerOrders::$STS` (خرده) — **اعداد یکسان، معانی متفاوت:**
|
||
|
||
| # | برچسب خرده | تعارض |
|
||
|---|---|---|
|
||
| `1` | ثبت سفارش | ✅ یکی |
|
||
| `2` | تایید مدیر سیستم | ✅ یکی |
|
||
| `3` | در حال تامین | ✅ یکی |
|
||
| `4` | در حال آماده سازی | ✅ یکی |
|
||
| `5` | **خروج از انبار** | 🔴 ≠ تحویل مسئول فروش |
|
||
| `6` | **ارسال** | 🔴 ≠ تحویل مشتری |
|
||
| `7` | **تحویل مشتری** | 🔴 ≠ کنسل |
|
||
|
||
**`7` در یکی «تحویل مشتری» و در دیگری «کنسل شده» است.**
|
||
|
||
`OrderStatus` در Prisma فقط enum عمده را دارد. پس اگر `orders_orders`
|
||
منتقل شود، **`sts=7` یعنی سفارشِ تحویلشده به `CANCELLED` تبدیل میشود** —
|
||
یک سفارش موفق به کنسلشده. و `5`/`6` معنای اشتباه میگیرند.
|
||
|
||
🔶 اگر تصمیم §۷.۲ «منتقل نشود» باشد، این خطر منتفی است. اگر «منتقل شود»،
|
||
**جدول نگاشت دوم و جداگانه** لازم است.
|
||
|
||
### ۷.۴ — اقلام سفارش
|
||
|
||
`order_items` (۲۱ ستون) → `OrderItem` (۱۱ فیلد).
|
||
|
||
| legacy | `OrderItem` |
|
||
|---|---|
|
||
| `rash_weight` | `rashWeightGramsSnapshot` |
|
||
| `percent` | `wagePercentSnapshot` |
|
||
| `discount` | `discountPercentSnapshot` |
|
||
| `w` | `computedWeightGrams` |
|
||
| `rial_pay` | `flatRialCharge` |
|
||
| `qty` | `quantity` |
|
||
| `variant_id` | `productVariantId` |
|
||
|
||
🔴 **`original_rash_weight` و `original_percent` خانه ندارند.**
|
||
|
||
وجودِ جفت `original_*` در legacy یعنی مقدار اولیه برای ممیزی نگه داشته
|
||
میشد — تا معلوم باشد وزن یا درصد **بعداً دستکاری شده است یا نه**.
|
||
`OrderItem` فقط یک نسخه دارد (`…Snapshot`). پس این تاریخچهی ممیزی
|
||
**در مهاجرت از بین میرود**. 🔶 تصمیم مالک.
|
||
|
||
🟡 بیخانهی دیگر: `product_id` (مقصد فقط `variantId` دارد),
|
||
`product_title`, `variant_title` (عنوان لحظهی خرید), `price`, `wage`,
|
||
`sts`, `note`.
|
||
|
||
⚠️ `product_title`/`variant_title` در legacy **snapshot عنوان** بودند —
|
||
اگر نام محصول بعداً عوض شود، فاکتور قدیمی نام قدیمی را نشان میداد. مقصد
|
||
عنوان را از رابطه میخواند، یعنی **فاکتورهای تاریخی با تغییر نام محصول
|
||
عوض میشوند**.
|
||
|
||
`orders_orderitem` (۲۹ ستون، خرده) — تابع تصمیم §۷.۲.
|
||
|
||
---
|
||
|
||
## ۸. اسناد مالی — `cashes` → `LedgerEntry`
|
||
|
||
`cashes` با ۳۳ ستون. `Cash::$TYPES` → `LedgerSourceType`:
|
||
|
||
| legacy | برچسب | `LedgerSourceType` |
|
||
|---|---|---|
|
||
| `9` | سفارش | `ORDER_STOCK` |
|
||
| `1` | آب شده | `REPAYMENT_MELTED_GOLD` |
|
||
| `2` | حواله | `REPAYMENT_TRANSFER` |
|
||
| `3` | چک | `REPAYMENT_CHEQUE` |
|
||
| `4` | پولی | `REPAYMENT_CASH` |
|
||
| — | — | `ORDER_PRE_ORDER` ➕ |
|
||
| — | — | `WALLET_BONUS` ➕ (D13) |
|
||
| — | — | `ADJUSTMENT` ➕ |
|
||
|
||
نگاشت تمیز است. `$STS`: `-1` بررسینشده · `1` ثبت سند.
|
||
`$CONFIRMED`: `0`/`1`.
|
||
|
||
🔴 **فقط اسناد `confirmed=1` در موجودی حساب میشوند** (D24). پس مهاجرت باید
|
||
`confirmed` را حفظ کند، نه اینکه همه را تأییدشده فرض کند.
|
||
|
||
### ستونهای مخصوص نوع سند — ✅ مقصد پوشش داده
|
||
|
||
`LedgerEntry` عمداً همین ستونها را دارد:
|
||
|
||
| legacy | `LedgerEntry` |
|
||
|---|---|
|
||
| `ayar` | `karatPerMille` (`Int?`) |
|
||
| `lab` | `labName` |
|
||
| `ang` | `hallmark` |
|
||
| `account_name` | `accountName` |
|
||
| `bank_name` | `bankName` |
|
||
| `to` | `transferTo` |
|
||
| `original_value` | `originalValue` |
|
||
| `price` | `priceRial` |
|
||
| `price_per_gram` | `pricePerGramRialAtEntry` |
|
||
| `recive_date` | `receivedAt` (`DateTime?`) |
|
||
| `note` | `note` |
|
||
| `confirmed` | `confirmed` + `confirmedAt` |
|
||
| `order_id` | `orderId` |
|
||
| `remain`/`input`/`output` | `amount` + `direction` |
|
||
|
||
🟡 بیخانه: `balance`, `value`, `no` (شمارهی سند), `proceed_at`, `title`,
|
||
`user_id`, `sts`, `discount`.
|
||
|
||
🔴 **`ayar` در مبدأ `varchar(20)` است، در مقصد `Int?`.** یعنی اگر عیار
|
||
بهصورت متن («۱۸ عیار» یا «750») ذخیره شده، تبدیل عددی لازم است و مقادیر
|
||
غیرعددی میشکنند.
|
||
|
||
🔴 **`recive_date` و `proceed_at` در مبدأ `varchar(255)` هستند، نه
|
||
`timestamp`** — به احتمال زیاد **تاریخ جلالی بهصورت رشتهی فارسی**. مقصد
|
||
`receivedAt` را `DateTime?` گرفته. پس تجزیهی جلالی → میلادی لازم است، نه
|
||
`CAST`. و `proceed_at` خانه ندارد.
|
||
|
||
### `login` → `Customer` با `kind=RETAIL`
|
||
|
||
`login` (۱۹ ستون) جدول مشتری خرده است (`fname`, `lname`, `percent`,
|
||
`user_type`, `birth_day`, `token_sms`). طبق D13 حفظ، بدون کار.
|
||
|
||
---
|
||
|
||
## ۹. کاتالوگ
|
||
|
||
### `product` → `Product` (۳۵ ستون)
|
||
|
||
نگاشتشدنی: `title`, `body_html`, `product_code`, `is_active`, `disabled`,
|
||
`brand_id`, `branch_id`, `min_gram`, `max_gram`, `show_type`,
|
||
`delivery_type`, `message_long_delivery`, `offer_day`, `is_exist`,
|
||
`has_warranty`, `desk_note`.
|
||
|
||
🟡 بیخانهی احتمالی: `metafields_global_title_tag`,
|
||
`metafields_global_description_tag`, `tags`, `meta`, `supplier_id`,
|
||
`published_at`, `number_of_purchase`, `count_fav`, `count_order`,
|
||
`count_view`, `inventory_id`, `indicator_id_id`.
|
||
|
||
⚠️ `indicator_id_id` و `mobile_pic_id` و `file_id` — سه ستون تصویر با
|
||
نامگذاری ناهمگون (`_id_id` دوتایی، بازماندهی Django). در Prisma
|
||
`ProductImage` با `ProductImageRole` (`FEATURED`, `FEATURED_MOBILE`,
|
||
`GALLERY`, `GALLERY_MOBILE`) جایشان را گرفته. **نگاشت سه ستون → نقشها
|
||
باید صریح نوشته شود.**
|
||
|
||
### `products_productvarient` → `ProductVariant` (۲۵ ستون)
|
||
|
||
⚠️ **غلط املایی در نام جدول: `varient` (صحیح: `variant`).** در SQL مهاجرت
|
||
باید عیناً `products_productvarient` نوشته شود.
|
||
|
||
🔴 **`Inventory` با I بزرگ.** ستون در دامپ `` `Inventory` int `` است، ولی
|
||
`ProductVariant.php:36` در `$fillable` نوشته `'inventory'` (کوچک).
|
||
|
||
MySQL در لینوکس نام **ستون** را case-insensitive میگیرد، پس در legacy کار
|
||
میکند. **Postgres اینطور نیست.** اگر کسی کوئری مهاجرت را با `inventory`
|
||
بنویسد، MySQL قبول میکند — ولی هر ابزار سختگیرتری خطا میدهد. **در
|
||
کوئریها `` `Inventory` `` با I بزرگ و داخل backtick نوشته شود.**
|
||
|
||
نگاشت بقیه: `grams`, `rial_pay` (اجرت ریالی/سنگ), `percentage_pay`,
|
||
`barcode`, `color` (`smallint` — جدول رنگ لازم), `qty`, `is_pre_order`,
|
||
`is_active`, `width`/`length`/`height`, `position`, `product_id_id`.
|
||
|
||
### `category_collection` → `Category` (۲۴ ستون)
|
||
|
||
`parent_id_id` → سلسلهمراتب. ⚠️ `image_id_id` **و** `image_id` **و**
|
||
`icon_id` — سه ستون تصویر، یکی با `_id_id` دوتایی. و `type` **و**
|
||
`category_type` هر دو وجود دارند.
|
||
|
||
🔶 **ترتیب ۹ دستهی خانه با id عددی هاردکد شده** (Q5 در `PWA-PORT-PLAN.md`).
|
||
جدول نگاشت `legacyId → id` برای این **اجباری** است.
|
||
|
||
`category_collect` (۹ ستون) → `CollectionProduct` — جدول واسط.
|
||
⚠️ سه FK دارد (`collection_id_id`, `post_id_id`, `product_id_id`) که
|
||
`post_id_id` در مقصد معادلی ندارد.
|
||
|
||
### `image` → `UploadedFile` (۲۰ ستون)
|
||
|
||
`image` (`char(100)`) مسیر فایل · `original_name`, `ext`, `width`, `height`.
|
||
🟡 بیخانه: `resize`, `resize_path`, `resize_watermark`,
|
||
`resize_watermark_path`, `resize_small`, `resize_small_path`, `banner`,
|
||
`position`, `link`.
|
||
|
||
legacy شش ستون برای نسخههای تغییراندازهیافته داشت. **اگر مقصد اینها را
|
||
ندارد، باید تصاویر بازتولید شوند یا فقط نسخهی اصلی بماند.**
|
||
|
||
### `cities` → `City` (۱۱ ستون)
|
||
|
||
`parent`, `level`, `has_children` → سلسلهمراتب.
|
||
|
||
⚠️ `percent` و `percent_aux` — **معنای چهارم `percent`!**
|
||
`MIGRATION-STATE.md` سه معنا را فهرست کرده؛ این چهارمی است: اضافهبهای اجرت
|
||
بهتفکیک شهر.
|
||
|
||
✅ **مقصد هر دو را دارد:** `City.percent` و `City.percentAux`، هر دو
|
||
`Decimal(6,3)?`، با کامنت صریح که از `cities.percent`/`percent_aux` آمدهاند.
|
||
نگاشت تمیز است.
|
||
|
||
### `branches`/`workshops` → `Branch`/`Workshop`
|
||
|
||
هر دو ۹ ستون، ساختار یکسان (`name`, `address`, `phone`, `user_id`,
|
||
`is_active`). نگاشت تمیز.
|
||
|
||
---
|
||
|
||
## ۱۰. بقیهی جداول زنده
|
||
|
||
| legacy | ستون | مقصد | یادداشت |
|
||
|---|---|---|---|
|
||
| `custom_orders` | ۱۶ | `PreOrder` | `weight`, `color`, `size`, `color_stone`, `color_mina`, `workshop_id` |
|
||
| `custom_order_logs` | | `PreOrderStatusEvent` | |
|
||
| `order_activity` | | `OrderStatusEvent` | |
|
||
| `comments` | ۹ | `Comment` | `reply_to` خودارجاع |
|
||
| `major_promotion` | ۱۵ | `Promotion` | `type_customer`, `use_only`, `discount_percent`/`discount_price` |
|
||
| `major_promotion_ignores` | | `PromotionExclusion` | |
|
||
| `promotion_products` | ۱۳ | `PromotionExclusion`? | 🔶 ابهام |
|
||
| `inventory_branches` | ۹ | `BranchAllocation` | `count_item`, `sum_weight` |
|
||
| `inventory_transactions` | ۹ | `BranchAllocationItem` | |
|
||
| `messages` | ۹ | `SmsMessage` | ⚠️ `massage` — غلط املایی |
|
||
| `notify` | ۱۳ | `Notification` + `NotificationRecipient` | `read`, `type_send` |
|
||
| `message_notification` | ۹ | `Notification` | 🔶 دو جدول اعلان |
|
||
| `config_statics` | ۶ | `Setting` | key/value · شامل `percent` کیف پول (D13) |
|
||
| `fav` | ۲ | ❌ **مدل ندارد** | `PWA-PORT-PLAN` میگوید جدول تازه لازم است |
|
||
| `logs` | ۱۲ | `CustomerActivityLog` | |
|
||
| `apk_log` | ۴ | 🟡 بیخانه | `ip`, `version` — آمار نسخهی اپ |
|
||
|
||
### جداول کوچکی که مقصد ندارند — ✅ بررسی شد
|
||
|
||
هر شش جدول مدل Eloquent دارند و `$fillable`شان خوانده شد:
|
||
|
||
| legacy | `$fillable` | چیست | مقصد |
|
||
|---|---|---|---|
|
||
| `fav` | `customer_id`, `product_id` | علاقهمندی | ❌ **ندارد** |
|
||
| `cinies` | `title`, `icon_id`, `is_active`, `description`, `collection_id` | «سینی» — گروهبندی نمایشی محصول | ❌ ندارد |
|
||
| `ciny_products` | `product_id`, `ciny_id` | واسط سینی↔محصول | ❌ ندارد |
|
||
| `kids` | `phone`, `name`, `num` | فهرست فرزندان (تبریک تولد؟) | ❌ ندارد |
|
||
| `links` | `url`, `counter` | شمارندهی کلیک لینک | ❌ ندارد |
|
||
| `request_fashions` | `name`, `mobile` | درخواست «مد» — فرم تماس | ❌ ندارد |
|
||
| `request_logs` | `customer_id`, `ip`, `device`, `data`, `user_agent`, `route` | لاگ درخواست | تقریباً `CustomerActivityLog` |
|
||
|
||
### ✅ تصمیم مالک ۲۰۲۶-۱۰-۰۶ — `fav` لازم است
|
||
|
||
> «بله» — علاقهمندیها مهماند.
|
||
|
||
**یک migration افزایشی لازم است.** چون additive است، با `PROTECTED.md` §G
|
||
تضادی ندارد و قابلیت موجودی را لمس نمیکند.
|
||
|
||
مدل پیشنهادی — سادهترین چیزی که legacy را پوشش میدهد:
|
||
|
||
```prisma
|
||
model CustomerFavorite {
|
||
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])
|
||
}
|
||
```
|
||
|
||
⚠️ `fav` در legacy **فقط دو ستون دارد و هیچ PK ندارد** — پس ردیف تکراری
|
||
ممکن است. `@@id([customerId, productId])` این را میبندد، ولی مهاجرت باید
|
||
`DISTINCT` بزند وگرنه روی کلید مرکب میشکند.
|
||
|
||
⚠️ `Product.countFav` در legacy یک شمارندهی denormalized بود
|
||
(`product.count_fav`). اگر مقصد آن را ندارد، از `CustomerFavorite` محاسبه
|
||
شود — نه اینکه صفر نشان داده شود (§۵.۲ گروه ب).
|
||
|
||
🔶 **`cinies`/`ciny_products` هنوز تصمیم ندارند.** ساختار نمایشیاند
|
||
(`title`, `icon_id`, `collection_id`) — اگر صفحهی خانهی اپ از آنها
|
||
میخواند، نبودشان چیدمان را خالی میکند. این **دادهی کاربر نیست**، پس
|
||
فوریتش کمتر از `fav` است.
|
||
|
||
`kids`, `links`, `request_fashions` کماهمیتاند و رهاکردنشان قابلدفاع است.
|
||
|
||
---
|
||
|
||
## ۱۰.۴ — نتایج پیشبررسی روی دادهی واقعی (۲۰۲۶-۱۰-۰۶)
|
||
|
||
خروجی [`migration-precheck.sql`](./migration-precheck.sql). این بخش **فرضهای
|
||
بخشهای قبل را با عدد واقعی تأیید یا رد میکند**.
|
||
|
||
### حجم کار — کوچکتر از انتظار
|
||
|
||
| جدول | زنده | حذفشدهی نرم | نکته |
|
||
|---|---|---|---|
|
||
| `customers` | **۱٬۶۱۶** | ۲۱ | |
|
||
| `users` | ۲۲ | ۰ | ادمینها |
|
||
| `orders` | ۵۰۸ | **۲۰۸** | ۲۹٪ حذفشده |
|
||
| `order_items` | ۱٬۰۶۷ | **۴۵۳** | ۳۰٪ |
|
||
| `cashes` | ۶۴۸ | **۲۶۵** | ۲۹٪ |
|
||
| `product` | ۳٬۲۸۷ | **۲٬۰۱۵** | **۳۸٪ حذفشده** |
|
||
| `variant` | ۷٬۴۷۴ | **۶٬۱۹۴** | 🔴 **۴۵٪ حذفشده** |
|
||
| `image` | **۱۸٬۰۷۷** | ۰ | فایلها باید کپی شوند |
|
||
| `messages` | **۲۴٬۴۸۰** | ۰ | بزرگترین جدول |
|
||
| `fav` | ۲۵۱ | — | |
|
||
| `permission` | ۷۲۸ | — | ۱۴۴ rid × چند `ut` |
|
||
|
||
🔴 **soft-delete حجم بالایی دارد** — ۶٬۱۹۴ واریانت و ۲٬۰۱۵ محصول حذفشدهاند.
|
||
§۵.۸ میگوید مقصد `deleted_at` ندارد. پس قاعدهی
|
||
`WHERE deleted_at IS NULL` **نه یک احتیاط، بلکه تفاوت بین ۱٬۲۸۰ و ۷٬۴۷۴
|
||
واریانت** است. بدون آن، کاتالوگ شش برابر میشود و محصولات حذفشده زنده
|
||
برمیگردند.
|
||
|
||
### ✅ خبر خوب — سه نگرانی منتفی شد
|
||
|
||
| نگرانی | پیشبینی | واقعیت |
|
||
|---|---|---|
|
||
| کد ملی تکراری | محتمل | **۰ گروه** ✅ |
|
||
| `name` ناسازگار با اجزا | نامعلوم | **فقط ۴ ردیف** ✅ |
|
||
| اجزای نام تهی | نامعلوم | **۰** ✅ — §۲ کامل تأیید شد |
|
||
| `fav` تکراری | محتمل | **۰** ✅ |
|
||
| `permission` تکراری | محتمل | **۰** ✅ |
|
||
| `ayar` غیرعددی | محتمل | **۰ از ۳۳** ✅ تبدیل به `Int` بیخطر |
|
||
| یتیم `city_id`/`image_id`/سفارش/سند | نامعلوم | **۰** ✅ |
|
||
| `customers_copy1/2` ردیف یکتا | محتمل | **۰** ✅ بیخطر نادیده بگیرید |
|
||
|
||
**§۲ قطعی شد:** `empty_fname=0`, `empty_lname=0`, `parts_empty_name_set=0`.
|
||
یعنی **هر ۱٬۵۹۵ ردیف اجزای نام پر دارند** و تجزیهی `name` مطلقاً لازم
|
||
نیست. D0 با اطمینان کامل تصحیح میشود.
|
||
|
||
### 🔴 سه مسدودکنندهی تأییدشده
|
||
|
||
**۱. موبایل تکراری — ۲۷ گروه، ۵۹ ردیف.**
|
||
`@unique` روی `mobile` **قطعاً میشکند**. تصمیم مالک: «مسئول فروش برنده است».
|
||
🔶 ولی اگر هر دو ردیفِ تکراری مشتری عادی باشند (نه کارشناس)، این قاعده
|
||
جواب نمیدهد. قاعدهی پشتیبان لازم است — پیشنهاد: **ردیف با
|
||
`updated_at` جدیدتر برنده، بقیه با پسوند در جدول نگاشت ثبت شوند تا دستی
|
||
بررسی شوند.**
|
||
|
||
**۲. کد ملی تهی — ۱٬۵۹۵ از ۱٬۵۹۵ ردیف... یعنی همه.**
|
||
`empty_ncode = 1595` و `n_customers = 1595`. **هیچ مشتری کد ملی ندارد.**
|
||
ولی Prisma:
|
||
```prisma
|
||
nationalCode String @unique // ← NOT NULL
|
||
```
|
||
🔴 **مهاجرت روی اولین ردیف میشکند.** و `@unique` روی رشتهی خالی یعنی
|
||
فقط **یک** ردیف میتواند `''` باشد.
|
||
|
||
🔶 **تصمیم فوری لازم:** `nationalCode` باید `String?` شود (nullable). این
|
||
یک migration افزایشی روی مقصد است. بدون آن مهاجرت مشتری **غیرممکن** است.
|
||
|
||
**۳. گِردکردن رخ میدهد — ۲۴ + ۳۲ ردیف.**
|
||
`gold_needs_rounding = 24` و `order_weight_needs_rounding = 32`. یعنی §۴
|
||
درست بود: **«صفر اختلاف» دستنیافتنی است** و آستانهی عددی لازم است.
|
||
|
||
مبنای تطبیق برای ابزار:
|
||
```
|
||
sum_gold_debt 427.5041945329501 ← ۱۶ رقم معنادار، شاهد خودِ مشکل
|
||
sum_rial_debt 250000
|
||
sum_credit_limit 423623.33999999997 ← .33999999997 یعنی 423623.34
|
||
n_nonzero_gold 35 n_negative_gold 12
|
||
```
|
||
⚠️ **۱۲ مشتری بدهی طلای منفی دارند** (طلبکارند). مقصد `Decimal` علامتدار
|
||
است، پس مشکلی نیست — ولی تطبیق باید علامت را هم چک کند.
|
||
|
||
### 🔴 `cashes` — همهی ستونهای مبلغ `NULL` هستند
|
||
|
||
```
|
||
cash_amount_needs_rounding NULL
|
||
confirmed_only sum_gold=NULL sum_rial=NULL n=375
|
||
```
|
||
|
||
`SUM()` فقط وقتی `NULL` برمیگرداند که **هیچ ردیف غیرNULL نباشد**. یعنی
|
||
`cashes.remain` و `cashes.rial_remain` در **هر ۶۴۸ ردیف** `NULL` هستند.
|
||
|
||
پس مبلغ سند مالی در این دو ستون **نیست**. جای دیگری است — `value`,
|
||
`original_value`, `input`/`output`, یا `price`. §۸ نگاشت
|
||
`remain/input/output → amount + direction` را پیشنهاد داده بود؛
|
||
**این نگاشت باید بازبینی شود.**
|
||
|
||
🔶 **یک کوئری تکمیلی لازم است** (§۱۰.۶) تا معلوم شود مبلغ در کدام ستون است.
|
||
بدون این، `LedgerEntry.amount` صفر یا `NULL` پر میشود و **کل دفتر مالی
|
||
خالی منتقل میشود**.
|
||
|
||
✅ نکتهی خوب: `confirmed=1` در ۳۷۵ و `confirmed=0` در ۸ ردیف — D24
|
||
(«فقط تأییدشدهها در موجودی») قابل اجراست. مجموع ۳۸۳ از ۶۴۸، پس ۲۶۵ ردیف
|
||
`confirmed` تهی دارند که با حذفشدهها همخوانی دارد.
|
||
|
||
### 🔴 رمز عبور — ۱٬۵۹۰ از ۱٬۵۹۵ تهی
|
||
|
||
```
|
||
pw_bcrypt 5 pw_other 0 pw_empty 1590
|
||
```
|
||
|
||
**تقریباً هیچ مشتری رمز ندارد.** منطقی است: ورود مشتری با **OTP** است، نه
|
||
رمز. ولی Prisma:
|
||
```prisma
|
||
passwordHash String // ← NOT NULL
|
||
```
|
||
🔴 **مهاجرت میشکند.** دو راه: `passwordHash` nullable شود، یا مهاجرت یک
|
||
hash تصادفیِ غیرقابلاستفاده بگذارد. **گزینهی دوم امنتر است** — ستون
|
||
اجباری میماند و هیچکس نمیتواند با رمز خالی وارد شود.
|
||
|
||
⚠️ آن ۵ رمز `$2y$` (bcrypt) هستند و مقصد هم bcrypt است، پس **بدون
|
||
تغییر منتقل میشوند**.
|
||
|
||
### enum ها — دو نکته
|
||
|
||
**`type IS NULL` → ۵۸ ردیف.** §۵.۳ هشدار داده بود. این ۵۸ بیصدا
|
||
`APP_SHOPKEEPER` میشوند. 🔶 تصمیم مالک: درست است یا قاعدهی دیگری لازم
|
||
است؟ (۵۸ از ۱٬۶۱۶ ≈ ۳.۶٪)
|
||
|
||
**`sts` و `active` مقدار `-1` دارند، نه `0`.**
|
||
`sts: 1→۱۱۹۱, -1→۴۰۴` و `active: 1→۱۳۶۸, -1→۲۲۷`.
|
||
نگاشت `1→true`, `-1→false` که §۵.۱ نوشته بود **درست است** ✅.
|
||
|
||
**`orders.sts`** هر ۷ مقدار `0..6` را دارد، هیچ `7` — پس نگاشت
|
||
`OrderStatus` کامل است ✅ و `CANCELLED` در عمده استفاده نشده.
|
||
|
||
**`cashes.type`** هر ۵ مقدار (`1,2,3,4,9`) موجودند و همه نگاشت دارند ✅.
|
||
|
||
### تکفروشی (به تأخیر افتاده) — تأیید خطر
|
||
|
||
```
|
||
orders_orders 520 live retail_sts: 7 → ۳ ردیف
|
||
```
|
||
**۳ ردیف `sts=7` وجود دارند** — یعنی خطر §۷.۳ واقعی بود: اگر روزی منتقل
|
||
شوند، ۳ سفارشِ «تحویل مشتری» به `CANCELLED` تبدیل میشوند. ثبت شد برای
|
||
آینده.
|
||
|
||
### ⚠️ ۱۴ یتیم در `fav`
|
||
|
||
`fav.customer_id → customers` ۱۴ یتیم دارد. چون `fav` خودش PK ندارد و FK
|
||
هم نیست، اینها ردیفهاییاند که مشتریشان حذف شده. **در مهاجرت باید
|
||
نادیده گرفته شوند** وگرنه FK مقصد میشکند.
|
||
|
||
---
|
||
|
||
## ۱۰.۵ — 🔴 مهاجرت `merge` است، نه `import`
|
||
|
||
**تصمیم مالک ۲۰۲۶-۱۰-۰۶:** دادهی حسابداری و تصویر واریانت در سیستم جدید
|
||
**واقعی است**. پس دیتابیس مقصد خالی نیست.
|
||
|
||
این فرض بنیادی سند را عوض میکند:
|
||
|
||
| | `import` (فرض اولیه) | **`merge` (واقعیت)** |
|
||
|---|---|---|
|
||
| مقصد | خالی | **دادهی واقعی دارد** |
|
||
| خطا در میانه | دور بریز، از نو | 🔴 **دادهی واقعی خراب شد** |
|
||
| تکرارپذیری | آزاد | فقط با idempotency واقعی |
|
||
| `UPDATE` کامل ردیف | بیخطر | 🔴 **ممنوع** |
|
||
|
||
### ستونهایی که ابزار مهاجرت هرگز نباید بنویسد
|
||
|
||
اینها در legacy **هیچ معادلی ندارند** و از سامانههای مقصد پر میشوند.
|
||
یک `upsert` معمولی یا `UPDATE` کامل، همه را `NULL` میکند.
|
||
|
||
| ستون | مدل | پر میشود از | اگر پاک شود |
|
||
|---|---|---|---|
|
||
| `accountingGoldBalance` | `Customer` | نرمافزار حسابداری (D27) | موجودی طلای مشتری گم میشود |
|
||
| `accountingRialBalance` | `Customer` | نرمافزار حسابداری | موجودی ریالی گم میشود |
|
||
| `accountingBalanceAt` | `Customer` | نرمافزار حسابداری | زمان آخرین sync گم میشود |
|
||
| `accountingStockWeight` | `Product` | نرمافزار حسابداری | موجودی وزنی کارساخته گم میشود |
|
||
| `accountingStockCount` | `Product` | نرمافزار حسابداری | موجودی تعدادی گم میشود |
|
||
| `accountingStockAt` | `Product` | نرمافزار حسابداری | زمان آخرین sync گم میشود |
|
||
| **`imageId`** | **`ProductVariant`** | **انتخاب دستی ادمین** | **تصویر هر واریانت گم میشود** |
|
||
|
||
بهعلاوه **سه جدول کامل** که هیچ مبدأ legacy ندارند و باید کاملاً نادیده
|
||
گرفته شوند: `AccountingLink`, `AccountingOutbox`, `AccountingIssue`.
|
||
|
||
### 🔴 `ProductVariant.imageId` — حدس زده نشود
|
||
|
||
`products_productvarient` در legacy **هیچ ستون تصویر ندارد** (۲۵ ستون،
|
||
بررسی شد). پس این قابلیت **مقصد-تنها** است و `PROTECTED.md` §B آن را
|
||
«additive only» اعلام کرده.
|
||
|
||
**قاعده: مهاجرت `imageId` را لمس نمیکند — نه مینویسد، نه `NULL` میکند.**
|
||
اگر ابزار بخواهد از `file_id` محصول حدس بزند، تصویرِ انتخابیِ ادمین با
|
||
حدس جایگزین میشود.
|
||
|
||
⚠️ `ProductImage` با `onDelete: SetNull` به واریانت وصل است. پس اگر مهاجرت
|
||
تصاویر محصول را **حذف و از نو بسازد** (الگوی رایج در sync)، `imageId` هر
|
||
واریانت بیصدا `NULL` میشود — بدون هیچ خطایی. **تصاویر باید
|
||
افزایشی بهروز شوند، نه جایگزین.**
|
||
|
||
### قاعدهی اجباری ابزار
|
||
|
||
> ابزار مهاجرت **فقط ستونهایی را مینویسد که نگاشت صریح legacy دارند.**
|
||
> فهرست ستونهای نوشتنی **سفید** است (allow-list)، نه سیاه.
|
||
|
||
همان الگوی «ارسال تفاضلی» که در `CHANGELOG-PORT-HISTORY.md` §۵ برای فرم
|
||
مشتری اعمال شد — چون همان اشتباه (`Object.assign` ناقص) آنجا هم چهار ستون
|
||
را پاک میکرد.
|
||
|
||
### ✅ کلید تطبیق — تصمیم مالک ۲۰۲۶-۱۰-۰۶
|
||
|
||
| سؤال | جواب |
|
||
|---|---|
|
||
| کلید تطبیق | **`mobile`** |
|
||
| در تعارض کدام برنده | **مسئول فروش** |
|
||
| اهمیت | **کم** — دادههای مقصد فعلاً روی لوکالاند |
|
||
|
||
**نتیجهی عملی:** چون دادهی مقصد لوکال است و اهمیتش کم، **ریسک `merge`
|
||
بسیار کمتر از آن است که §۱۰.۵ فرض کرده بود.** ولی قاعدهی allow-list
|
||
**همچنان لازم است**، به دو دلیل:
|
||
|
||
1. همان ابزار بعداً روی **staging و تولید** اجرا میشود (`GO-LIVE §۸`)،
|
||
آنجا داده واقعی است
|
||
2. تصویر واریانت و اتصال حسابداری همین الان **واقعی** هستند (گفتهی مالک)
|
||
|
||
⚠️ **تداخل `mobile` با کلیدِ تطبیق تعارض دارد.** ۲۷ گروه موبایل تکراری در
|
||
مبدأ وجود دارد (§۱۰.۴). اگر کلید تطبیق `mobile` باشد، **برای ۵۹ ردیف
|
||
معلوم نیست به کدام ردیف مقصد وصل شوند.** قاعدهی «مسئول فروش برنده» فقط
|
||
۱۰ مورد تداخل `customers`↔`users` را حل میکند، نه ۲۷ گروه داخلی را.
|
||
|
||
🔶 قاعدهی پشتیبان پیشنهادی: در گروه تکراری، ردیف با `updated_at` جدیدتر
|
||
برنده؛ بقیه منتقل میشوند ولی در گزارش «نیاز به بررسی دستی» فهرست شوند.
|
||
چون اهمیت کم است، میتوان این را به گزارش سپرد و دستی حل کرد.
|
||
|
||
---
|
||
|
||
## ۱۰.۶ — پرسش دوم از دیتابیس
|
||
|
||
📄 **[`migration-precheck-2.sql`](./migration-precheck-2.sql)** — چهار سؤالی
|
||
که خروجی اول باز گذاشت. مثل فایل اول: فقط `COUNT`/`SUM`، صفر نوشتن، هیچ
|
||
ستون متنی در خروجی (تنها استثنا: `products_productimage.w` که یک برچسب
|
||
اندازه است، نه دادهی شخصی).
|
||
|
||
| # | سؤال | چرا مسدودکننده است |
|
||
|---|---|---|
|
||
| ۱ | **مبلغ سند مالی در کدام ستون است؟** | `remain`/`rial_remain` همه `NULL`اند. بدون جواب، `LedgerEntry.amount` خالی میماند و **کل دفتر مالی بیمبلغ منتقل میشود** |
|
||
| ۲ | آیا «مسئول فروش برنده» برای ۲۷ گروه تکراری کافی است؟ | اگر گروهی کارشناس نداشته باشد، قاعده بیاثر است |
|
||
| ۳ | سه ستون تصویر → چهار نقش `ProductImageRole` | نگاشت غلط = تصویر غلط روی محصول |
|
||
| ۴ | تأیید §۵.۷ — `reagent_id` به `customers` اشاره میکند؟ | اگر غلط باشد، رابطهی کارشناس معرف از بین میرود |
|
||
|
||
---
|
||
|
||
## ۱۱. جمعبندی — کارهای لازم قبل از نوشتن ابزار
|
||
|
||
### 🔴 مسدودکننده (بدون اینها ابزار در میانه میشکند یا داده خراب میکند)
|
||
|
||
| # | مورد | § |
|
||
|---|---|---|
|
||
| ۱ | ~~شمارش تکرار~~ | ✅ **انجام شد** — ۲۷ گروه موبایل، ۰ کد ملی (§۱۰.۴) |
|
||
| ۱.۱ | 🔴 **`nationalCode` باید nullable شود** — هر ۱٬۵۹۵ ردیف تهی است | ۱۰.۴ |
|
||
| ۱.۲ | 🔴 **`passwordHash`** — ۱٬۵۹۰ تهی؛ hash تصادفی یا nullable | ۱۰.۴ |
|
||
| ۱.۳ | 🔴 **مبلغ سند مالی کجاست؟** `cashes.remain` همه `NULL` | ۱۰.۶ |
|
||
| ۱.۴ | 🔴 **`VerificationStep` حالت `REJECTED` ندارد** — ۱۴ ردیف | ۵.۵ |
|
||
| ۱.۵ | 🔴 **ترتیب دومرحلهای** — کارشناسان قبل از مشتریان | ۵.۷ |
|
||
| ۲ | آستانهی تطبیق عددی جای «صفر اختلاف» — **تأیید شد: ۵۶ ردیف گِرد میشوند** | ۴ · ۱۰.۴ |
|
||
| ۳ | قاعدهی `WHERE deleted_at IS NULL` — **حیاتیتر از انتظار: ۴۵٪ واریانتها** | ۵.۸ · ۱۰.۴ |
|
||
| ۴ | ~~تصمیم `orders_orders`~~ | ✅ **حل — به تأخیر افتاد** (§۷.۲) |
|
||
| ۵ | تصحیح D0: `fname`/`lname` مستقیم، تجزیه نشود | ۲ |
|
||
| ۶ | **allow-list ستونهای نوشتنی** — ۷ ستون مقصد-تنها لمس نشوند | **۱۰.۵** |
|
||
| ۷ | **کلید تطبیق ردیفهای موجود مقصد** | **۱۰.۵** |
|
||
|
||
### 🔶 تصمیم مالک
|
||
|
||
| # | مورد | § |
|
||
|---|---|---|
|
||
| ۱ | ۴ ستون دادهی غیرقابلبازسازی (`birth_date`, `marriage_date`, `token_mobile`, `new_city`) | ۵.۲ |
|
||
| ۲ | `verificationStep` — تفکیک `STEP_TWO_SUBMITTED` از `VERIFIED` | ۵.۵ |
|
||
| ۳ | `type IS NULL` چه `CustomerKind` شود | ۵.۳ |
|
||
| ۴ | ~~`fav`~~ | ✅ **حل — لازم است، migration افزایشی** (§۱۰) |
|
||
| ۴.۱ | `cinies`/`ciny_products` — چیدمان نمایشی | ۱۰ |
|
||
| ۵ | شمارندههای denormalized محاسبه شوند یا صفر بمانند | ۵.۲ |
|
||
| ۶ | نسخههای تغییراندازهیافتهی تصویر | ۹ |
|
||
| ۷ | `original_rash_weight`/`original_percent` — تاریخچهی ممیزی قلم سفارش | ۷.۴ |
|
||
| ۸ | snapshot عنوان محصول در فاکتورهای تاریخی | ۷.۴ |
|
||
|
||
### ✅ یافتههای جانبی (ثبت، بدون کار)
|
||
|
||
- `users.permission decimal(16,16)` ستونی است که ریاضیاً کار نمیکند — مرده (§۶)
|
||
- `products_productvarient.Inventory` با I بزرگ، برخلاف `$fillable` (§۹)
|
||
- نام جدول `varient` غلط املایی دارد (§۹)
|
||
- `messages.massage` غلط املایی دارد (§۱۰)
|
||
- `cities.percent` معنای **چهارم** `percent` است (§۹)
|
||
- `orders_histroy` غلط املایی دارد
|
||
- صفر foreign key در کل ۸۰ جدول — یکپارچگی ارجاعی در اپ بود، نه دیتابیس
|
||
|
||
---
|
||
|
||
## ۱۲. آنچه این سند تأیید نمیکند
|
||
|
||
⚠️ **دامپ داده ندارد.** هر عددی در این سند از *ساختار* آمده. تعداد ردیف،
|
||
تکرار، تهیبودن و اختلاف محاسباتی **همه دادهی واقعی لازم دارند**.
|
||
|
||
⚠️ **`ayar` و `recive_date`/`proceed_at` بر اساس *نوع* ستون تحلیل شدند، نه
|
||
محتوا.** اینکه عیار متنی است یا عددی، و تاریخ جلالی است یا میلادی، فقط با
|
||
دیدن داده قطعی میشود.
|
||
|
||
⚠️ **ستونبهستونِ کامل فقط برای `customers` نوشته شد** (۴۹/۴۹). بقیهی
|
||
جداول در سطح «نگاشتشدنی / بیخانه / مبهم» بررسی شدند. برای جداول بزرگ
|
||
(`cashes` ۳۳، `product` ۳۵، `orders_orders` ۳۷) اگر لازم است، ستونبهستون
|
||
در ادامه نوشته میشود.
|
||
|
||
⚠️ **موتور مقصد Postgres است و مبدأ MySQL** — تفاوتهای حساسیت به بزرگی
|
||
حرف، `tinyint(1)` در برابر `boolean`، و `timestamp` با/بدون منطقهی زمانی
|
||
هنگام نوشتن ابزار باید یکبهیک بررسی شوند.
|
||
|
||
---
|
||
|
||
## مرجع
|
||
|
||
- [`GO-LIVE-PLAN.md`](./GO-LIVE-PLAN.md) §۷ — ابزار مهاجرت و شرطهای تطبیق
|
||
- [`parity/DECISIONS.md`](./parity/DECISIONS.md) — D0, D9, D13, D17, D21, D24
|
||
- [`parity/MIGRATION-STATE.md`](./parity/MIGRATION-STATE.md) — سه معنای `percent`
|
||
- [`parity/ISSUES.md`](./parity/ISSUES.md) — MON-07 (دو enum وضعیت)
|
||
- [`PWA-PORT-PLAN.md`](./PWA-PORT-PLAN.md) — Q5 (ترتیب دستهها)، نقص #۱۹
|