arc/docs/BUSINESS_MIGRATION_SCENARIO.md
2026-04-14 19:34:55 +03:30

491 lines
19 KiB
Markdown
Executable file

# سناریوی انتقال کسب و کارها از hesabixOld به hesabixpy
## خلاصه اجرایی
این سند سناریوی کامل انتقال کسب و کارها (businesses) از دیتابیس قدیمی (`hesabixOld`) به دیتابیس جدید (`hesabixpy`) را ارائه می‌دهد. تمرکز اصلی بر روی نحوه انتقال کسب و کارها و نگاشت صحیح owner_id به کاربران منتقل شده است.
## وضعیت فعلی
### دیتابیس قدیمی (hesabixOld)
- **جدول**: `business`
- **تعداد کسب و کارها**: 4,473 کسب و کار
- **فیلدهای کلیدی**:
- `id`: شناسه یکتا
- `owner_id`: شناسه مالک (ارجاع به جدول user)
- `name`: نام کسب و کار
- `legal_name`: نام قانونی
- `field`: زمینه فعالیت (varchar - مقادیر مختلف)
- `type`: نوع کسب و کار (varchar - مقادیر: فروشگاه، مغازه، شخصی، شرکت، موسسه، باشگاه، اتحادیه)
- `shenasemeli`: شناسه ملی
- `codeeghtesadi`: کد اقتصادی
- `shomaresabt`: شماره ثبت
- `country`: کشور
- `ostan`: استان
- `shahrestan`: شهرستان
- `postalcode`: کد پستی
- `tel`: تلفن
- `mobile`: موبایل
- `address`: آدرس
- `email`: ایمیل
- `wesite`: وب‌سایت (با typo)
- و فیلدهای تنظیمات دیگر...
### دیتابیس جدید (hesabixpy)
- **جدول**: `businesses`
- **تعداد کسب و کارهای فعلی**: 63 کسب و کار
- **فیلدهای کلیدی**:
- `id`: شناسه یکتا
- `owner_id`: شناسه مالک (ارجاع به جدول users - ForeignKey)
- `name`: نام کسب و کار
- `business_type`: نوع کسب و کار (ENUM: شرکت، مغازه، فروشگاه، اتحادیه، باشگاه، موسسه، شخصی)
- `business_field`: زمینه فعالیت (ENUM: تولیدی، بازرگانی، خدماتی، سایر)
- `national_id`: شناسه ملی
- `registration_number`: شماره ثبت
- `economic_id`: کد اقتصادی
- `country`: کشور
- `province`: استان
- `city`: شهرستان
- `postal_code`: کد پستی
- `phone`: تلفن
- `mobile`: موبایل
- `address`: آدرس
- `default_currency_id`: ارز پیش‌فرض
- `logo_file_id`: شناسه فایل لوگو
- `stamp_file_id`: شناسه فایل مهر
- `default_credit_limit`: سقف اعتبار پیش‌فرض
- `check_credit_enabled_by_default`: بررسی اعتبار به صورت پیش‌فرض
- `created_at`: تاریخ ایجاد
- `updated_at`: تاریخ به‌روزرسانی
- `deleted_at`: تاریخ حذف (soft delete)
### مشکلات شناسایی شده
1. **نگاشت owner_id**: باید از شناسه کاربر قدیمی به شناسه کاربر جدید نگاشت شود
2. **تبدیل type و field**: باید از varchar به ENUM تبدیل شوند
3. **تفاوت در نام فیلدها**: `ostan` → `province`, `shahrestan` → `city`, `tel` → `phone`
4. **فیلدهای جدید**: `default_currency_id`, `logo_file_id`, `stamp_file_id` و... در قدیمی وجود ندارند
5. **37 کسب و کار** از دیتابیس قدیمی در دیتابیس جدید وجود دارند (احتمالاً کسب و کارهای تست)
### آمار و ارقام
- **کل کسب و کارها در قدیمی**: 4,473
- **کسب و کارها با owner_id**: 4,473 (همه کسب و کارها owner دارند)
- **کسب و کارها با owner معتبر**: 4,415 (با کاربر فعال و email/mobile)
- **کسب و کارهای قابل انتقال**: ~4,062 (با owner که در دیتابیس جدید وجود دارد)
- **کسب و کارهای موجود در جدید**: 63
### توزیع نوع کسب و کارها (type)
- مغازه: 1,410
- شرکت: 1,121
- شخصی: 1,003
- فروشگاه: 718
- موسسه: 182
- باشگاه: 26
- اتحادیه: 13
## سناریوی انتقال
### مرحله 1: آماده‌سازی و بررسی
#### 1.1 بررسی داده‌های قدیمی
```sql
-- بررسی کسب و کارها با owner_id معتبر
SELECT COUNT(*) FROM hesabixOld.business WHERE owner_id IS NOT NULL;
-- بررسی کسب و کارها با owner_id که در دیتابیس جدید وجود دارد
SELECT COUNT(*)
FROM hesabixOld.business old
INNER JOIN hesabixpy.users new ON old.owner_id = (
SELECT old_user.id
FROM hesabixOld.user old_user
INNER JOIN hesabixpy.users new_user ON old_user.email = new_user.email
WHERE old_user.id = old.owner_id
LIMIT 1
);
-- بررسی توزیع type
SELECT type, COUNT(*) as cnt
FROM hesabixOld.business
WHERE type IS NOT NULL
GROUP BY type
ORDER BY cnt DESC;
-- بررسی توزیع field
SELECT field, COUNT(*) as cnt
FROM hesabixOld.business
WHERE field IS NOT NULL
GROUP BY field
ORDER BY cnt DESC
LIMIT 20;
```
#### 1.2 ایجاد جدول mapping برای owner_id
```sql
-- ایجاد جدول موقت برای نگهداری mapping کاربران
CREATE TEMPORARY TABLE user_id_mapping AS
SELECT
old.id as old_user_id,
new.id as new_user_id
FROM hesabixOld.user old
INNER JOIN hesabixpy.users new ON (
old.email = new.email OR
(old.mobile IS NOT NULL AND new.mobile IS NOT NULL AND old.mobile = new.mobile)
)
WHERE old.active = 1;
```
### مرحله 2: تبدیل و نگاشت داده‌ها
#### 2.1 تبدیل فیلدها
| فیلد قدیمی | فیلد جدید | تبدیل |
|------------|-----------|-------|
| `id` | - | نگه‌داری در mapping table (اختیاری) |
| `owner_id` | `owner_id` | نگاشت از user_id_mapping |
| `name` | `name` | مستقیم |
| `legal_name` | - | حذف (یا در name نگه داریم) |
| `type` | `business_type` | تبدیل به ENUM |
| `field` | `business_field` | تبدیل به ENUM |
| `shenasemeli` | `national_id` | مستقیم |
| `codeeghtesadi` | `economic_id` | مستقیم |
| `shomaresabt` | `registration_number` | مستقیم |
| `country` | `country` | مستقیم |
| `ostan` | `province` | مستقیم |
| `shahrestan` | `city` | مستقیم |
| `postalcode` | `postal_code` | مستقیم |
| `tel` | `phone` | مستقیم |
| `mobile` | `mobile` | مستقیم |
| `address` | `address` | مستقیم |
| `email` | - | حذف (در جدول users است) |
| `wesite` | - | حذف (در صورت نیاز می‌توان در جدول جداگانه نگه داشت) |
#### 2.2 تبدیل type به business_type (ENUM)
**مقادیر قدیمی** → **مقادیر جدید**:
- `"فروشگاه"` → `BusinessType.STORE` (`"فروشگاه"`)
- `"مغازه"` → `BusinessType.SHOP` (`"مغازه"`)
- `"شخصی"` → `BusinessType.INDIVIDUAL` (`"شخصی"`)
- `"شرکت"` → `BusinessType.COMPANY` (`"شرکت"`)
- `"موسسه"` → `BusinessType.INSTITUTE` (`"موسسه"`)
- `"باشگاه"` → `BusinessType.CLUB` (`"باشگاه"`)
- `"اتحادیه"` → `BusinessType.UNION` (`"اتحادیه"`)
- `NULL` یا مقادیر نامعتبر → `BusinessType.SHOP` (پیش‌فرض)
**الگوریتم**:
```python
def convert_business_type(old_type: str | None) -> str:
mapping = {
"فروشگاه": "فروشگاه",
"مغازه": "مغازه",
"شخصی": "شخصی",
"شرکت": "شرکت",
"موسسه": "موسسه",
"باشگاه": "باشگاه",
"اتحادیه": "اتحادیه"
}
if old_type and old_type in mapping:
return mapping[old_type]
return "مغازه" # پیش‌فرض
```
#### 2.3 تبدیل field به business_field (ENUM)
**مقادیر قدیمی** → **مقادیر جدید**:
- `"تولید"`, `"تولیدی"`, `"تولید کننده"` → `BusinessField.MANUFACTURING` (`"تولیدی"`)
- `"بازرگانی"`, `"فروش"`, `"فروشگاه"` → `BusinessField.TRADING` (`"بازرگانی"`)
- `"خدماتی"` → `BusinessField.SERVICE` (`"خدماتی"`)
- سایر مقادیر → `BusinessField.OTHER` (`"سایر"`)
**الگوریتم**:
```python
def convert_business_field(old_field: str | None) -> str:
if not old_field:
return "سایر"
old_field_lower = old_field.lower().strip()
# تولیدی
if any(keyword in old_field_lower for keyword in ["تولید", "ساخت"]):
return "تولیدی"
# بازرگانی
if any(keyword in old_field_lower for keyword in ["بازرگانی", "فروش", "خرید", "تجارت"]):
return "بازرگانی"
# خدماتی
if any(keyword in old_field_lower for keyword in ["خدمات", "خدماتی", "مشاوره", "آموزش"]):
return "خدماتی"
# سایر
return "سایر"
```
#### 2.4 نگاشت owner_id
**الگوریتم**:
1. جستجو در `user_id_mapping` برای یافتن `new_user_id` بر اساس `old_owner_id`
2. اگر پیدا شد: استفاده از آن
3. اگر پیدا نشد: skip کردن کسب و کار (مالک در دیتابیس جدید وجود ندارد)
**SQL برای ایجاد mapping**:
```sql
-- ایجاد جدول mapping کاربران
CREATE TEMPORARY TABLE user_id_mapping AS
SELECT
old.id as old_user_id,
new.id as new_user_id
FROM hesabixOld.user old
INNER JOIN hesabixpy.users new ON (
(old.email IS NOT NULL AND new.email IS NOT NULL AND old.email = new.email) OR
(old.mobile IS NOT NULL AND new.mobile IS NOT NULL AND old.mobile = new.mobile)
)
WHERE old.active = 1;
```
### مرحله 3: الگوریتم انتقال
#### 3.1 فیلتر کسب و کارها برای انتقال
**شرایط انتقال**:
1. کسب و کار باید `owner_id` داشته باشد
2. `owner_id` باید در `user_id_mapping` وجود داشته باشد (یعنی کاربر منتقل شده باشد)
3. کسب و کار نباید در دیتابیس جدید وجود داشته باشد (بر اساس owner_id و name)
**SQL برای انتخاب کسب و کارها**:
```sql
SELECT b.*
FROM hesabixOld.business b
INNER JOIN user_id_mapping m ON b.owner_id = m.old_user_id
WHERE b.owner_id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM hesabixpy.businesses new
WHERE new.owner_id = m.new_user_id
AND new.name = b.name
)
ORDER BY b.id;
```
#### 3.2 پردازش هر کسب و کار
**مراحل پردازش**:
1. **بررسی owner_id**:
- جستجو در `user_id_mapping`
- اگر پیدا نشد: skip با reason "owner_not_migrated"
2. **تبدیل داده‌ها**:
- تبدیل `type` به `business_type` (ENUM)
- تبدیل `field` به `business_field` (ENUM)
- تبدیل نام فیلدها (`ostan` → `province`, `shahrestan` → `city`, `tel` → `phone`)
- normalize کردن `mobile` و `phone`
- تبدیل `date_submit` به `created_at` (اگر وجود دارد)
3. **ایجاد کسب و کار جدید**:
- درج در جدول `businesses`
- تنظیم `owner_id` به `new_user_id`
- تنظیم `created_at` و `updated_at`
4. **مدیریت خطاها**:
- در صورت خطا: ثبت در لاگ با جزئیات
- ادامه با کسب و کار بعدی
#### 3.3 مدیریت کسب و کارهای تکراری
**سناریو 1: کسب و کار با همان owner و name در دیتابیس جدید وجود دارد**
- بررسی: آیا کسب و کار با همان `owner_id` و `name` در دیتابیس جدید وجود دارد؟
- عمل: skip کردن با reason "already_exists"
- ثبت در لاگ
**سناریو 2: owner_id در دیتابیس جدید وجود ندارد**
- بررسی: آیا `owner_id` در `user_id_mapping` وجود دارد؟
- عمل: skip کردن با reason "owner_not_migrated"
- ثبت در لاگ
### مرحله 4: فیلدهای اختیاری و پیش‌فرض
#### 4.1 فیلدهای جدید در دیتابیس جدید
**فیلدهایی که در قدیمی وجود ندارند**:
- `default_currency_id`: `NULL` (می‌توان بعداً تنظیم کرد)
- `logo_file_id`: `NULL` (فایل‌ها باید جداگانه منتقل شوند)
- `stamp_file_id`: `NULL` (فایل‌ها باید جداگانه منتقل شوند)
- `default_credit_limit`: `NULL` (می‌توان بعداً تنظیم کرد)
- `check_credit_enabled_by_default`: `false` (پیش‌فرض)
#### 4.2 تبدیل تاریخ‌ها
**الگوریتم**:
- اگر `date_submit` وجود دارد: تبدیل به `datetime` و استفاده در `created_at`
- در غیر این صورت: استفاده از `datetime.utcnow()`
- `updated_at`: همیشه `datetime.utcnow()`
### مرحله 5: اسکریپت انتقال
#### 5.1 ساختار اسکریپت
**فایل**: `scripts/migrate_businesses_from_old_db.py`
**ویژگی‌ها**:
1. اتصال به هر دو دیتابیس
2. ایجاد `user_id_mapping` از کاربران منتقل شده
3. خواندن کسب و کارها از دیتابیس قدیمی
4. تبدیل و نگاشت داده‌ها
5. درج در دیتابیس جدید
6. لاگ‌گیری کامل
7. قابلیت dry-run (تست بدون تغییر)
8. پردازش batch به batch
#### 5.2 پارامترهای اسکریپت
```bash
python scripts/migrate_businesses_from_old_db.py [OPTIONS]
```
**پارامترها**:
- `--dry-run`: اجرای تست بدون تغییر در دیتابیس
- `--batch-size`: تعداد کسب و کارها در هر batch (پیش‌فرض: 100)
- `--start-id`: شروع از شناسه خاص
- `--limit`: محدود کردن تعداد کسب و کارها
- `--old-db`: نام دیتابیس قدیمی (پیش‌فرض: hesabixOld)
- `--new-db`: نام دیتابیس جدید (پیش‌فرض: hesabixpy)
- `--db-user`: نام کاربری دیتابیس (پیش‌فرض: root)
- `--db-password`: رمز عبور دیتابیس (پیش‌فرض: 136431)
- `--db-host`: آدرس دیتابیس (پیش‌فرض: localhost)
- `--db-port`: پورت دیتابیس (پیش‌فرض: 3306)
#### 5.3 خروجی و گزارش
**گزارش شامل**:
- تعداد کل کسب و کارهای پردازش شده
- تعداد کسب و کارهای منتقل شده
- تعداد کسب و کارهای skip شده (با دلایل):
- `owner_not_migrated`: مالک در دیتابیس جدید وجود ندارد
- `already_exists`: کسب و کار در دیتابیس جدید وجود دارد
- `invalid_data`: داده‌های نامعتبر
- تعداد خطاها
- لیست خطاها با جزئیات
### مرحله 6: تست و اعتبارسنجی
#### 6.1 تست‌های واحد
1. **تست تبدیل business_type**:
- ورودی: `"فروشگاه"` → خروجی: `"فروشگاه"`
- ورودی: `"مغازه"` → خروجی: `"مغازه"`
- ورودی: `NULL` → خروجی: `"مغازه"` (پیش‌فرض)
2. **تست تبدیل business_field**:
- ورودی: `"تولیدی"` → خروجی: `"تولیدی"`
- ورودی: `"بازرگانی"` → خروجی: `"بازرگانی"`
- ورودی: `"خدماتی"` → خروجی: `"خدماتی"`
- ورودی: `"کامپیوتری"` → خروجی: `"سایر"`
3. **تست نگاشت owner_id**:
- تست با owner_id موجود در mapping
- تست با owner_id غیرموجود در mapping
#### 6.2 تست یکپارچگی
1. **تست انتقال نمونه**:
- انتخاب 10 کسب و کار نمونه از دیتابیس قدیمی
- انتقال آنها
- بررسی صحت داده‌ها
- بررسی foreign key constraint (owner_id)
2. **تست تکراری بودن**:
- تست skip کردن کسب و کارهای تکراری
#### 6.3 تست عملکرد
1. **تست سرعت**:
- اندازه‌گیری زمان انتقال 1000 کسب و کار
- بهینه‌سازی در صورت نیاز
### مرحله 7: اجرای نهایی
#### 7.1 آماده‌سازی
1. **پشتیبان‌گیری**:
- پشتیبان از دیتابیس `hesabixpy`
- پشتیبان از دیتابیس `hesabixOld`
2. **تست در محیط staging**:
- اجرای کامل در محیط تست
- بررسی نتایج
#### 7.2 اجرا
1. **اجرای dry-run**:
```bash
python scripts/migrate_businesses_from_old_db.py --dry-run --verbose
```
2. **بررسی نتایج dry-run**:
- بررسی تعداد کسب و کارها
- بررسی خطاها
- بررسی owner_id mapping
3. **اجرای واقعی**:
```bash
python scripts/migrate_businesses_from_old_db.py --batch-size 100 --verbose
```
4. **نظارت**:
- نظارت بر لاگ‌ها
- بررسی خطاها
- بررسی عملکرد
#### 7.3 پس از اجرا
1. **اعتبارسنجی**:
- مقایسه تعداد کسب و کارها
- بررسی foreign key constraint
- بررسی یکپارچگی داده‌ها
- تست ایجاد کسب و کار جدید
2. **پاکسازی**:
- حذف جدول موقت `user_id_mapping` (خودکار)
## خلاصه مراحل
1. ⏳ بررسی و آماده‌سازی داده‌ها
2. ⏳ ایجاد user_id_mapping
3. ⏳ نوشتن اسکریپت انتقال
4. ⏳ تست در محیط staging (با --dry-run)
5. ⏳ پشتیبان‌گیری
6. ⏳ اجرای انتقال
7. ⏳ اعتبارسنجی
## نکات مهم
1. **نگاشت owner_id**: فقط کسب و کارهایی منتقل می‌شوند که مالک آن‌ها در دیتابیس جدید وجود دارد
2. **تبدیل ENUM**: type و field باید به درستی به ENUM تبدیل شوند
3. **کاربران تکراری**: کسب و کارهایی که در دیتابیس جدید وجود دارند را skip می‌کنیم
4. **فیلدهای جدید**: فیلدهای جدید در دیتابیس جدید با مقادیر پیش‌فرض یا NULL تنظیم می‌شوند
5. **لاگ‌گیری**: تمام مراحل را لاگ می‌کنیم برای audit و debug
## ریسک‌ها و راه‌حل‌ها
### ریسک 1: از دست رفتن داده‌ها
**راه‌حل**: پشتیبان‌گیری کامل قبل از انتقال
### ریسک 2: خطا در انتقال
**راه‌حل**: استفاده از transaction و rollback در صورت خطا
### ریسک 3: کسب و کارهای تکراری
**راه‌حل**: بررسی دقیق قبل از انتقال و skip کردن تکراری‌ها
### ریسک 4: owner_id نامعتبر
**راه‌حل**: بررسی وجود owner_id در user_id_mapping قبل از انتقال
### ریسک 5: تبدیل نامعتبر ENUM
**راه‌حل**: استفاده از مقادیر پیش‌فرض برای مقادیر نامعتبر
## نتیجه‌گیری
این سناریو راهنمای کامل انتقال کسب و کارها از دیتابیس قدیمی به جدید است. تمرکز اصلی بر روی:
- نگاشت صحیح owner_id به کاربران منتقل شده
- تبدیل صحیح type و field به ENUM
- مدیریت کسب و کارهای تکراری
- حفظ یکپارچگی داده‌ها
پس از اجرای موفق، کسب و کارها با مالک‌های صحیح در دیتابیس جدید ایجاد می‌شوند و می‌توانند استفاده شوند.