# دیتابیس

نمودار ERD و فهرست قیود: [design/02-erd.md](design/02-erd.md)

## قراردادها

- موتور InnoDB، `utf8mb4_unicode_ci`.
- کلید اصلی `id BIGINT UNSIGNED AUTO_INCREMENT`؛ کلیدهای خارجی `BIGINT UNSIGNED`.
- **پول**: `BIGINT` به تومان. هیچ ستون مالی `FLOAT/DOUBLE` نیست. مقادیر رمزارزی `DECIMAL(36,18)`.
- **زمان**: `DATETIME` به UTC (اتصال با `time_zone = '+00:00'`). نمایش به وقت `APP_TIMEZONE` و تقویم جلالی.
- **وضعیت‌ها**: `VARCHAR` + Enum در PHP (`CampaignStatus`, `OrderStatus`, …)؛ افزودن وضعیت جدید `ALTER TABLE` نمی‌خواهد.
- JSON در `LONGTEXT` (سازگار با MariaDB).
- حالت اتصال: `STRICT_ALL_TABLES`، `READ COMMITTED`، Prepared Statement واقعی (بدون Emulation).

## گروه‌های جدول

| ماژول | جداول |
|---|---|
| هسته | `bots`, `users`, `bot_users`, `settings`, `telegram_updates`, `failed_jobs`, `daily_stats`, `migrations` |
| کیف پول | `wallets`, `wallet_transactions` |
| کانال | `channels`, `channel_members`, `channel_links` |
| تبلیغات | `products`, `campaigns`, `campaign_links`, `campaign_members` |
| سفارش و پرداخت | `orders`, `order_items`, `payments`, `payment_transactions` |
| برداشت و هویت | `withdrawals`, `bank_accounts`, `crypto_wallets`, `kyc_requests` |
| زیرمجموعه | `referrals`, `referral_transactions` |
| ارتباطات | `tickets`, `ticket_messages`, `notifications`, `broadcasts`, `broadcast_recipients` |
| مدیریت | `admins`, `roles`, `permissions`, `role_permissions`, `admin_roles`, `audit_logs`, `risk_events` |

## یکپارچگی داده‌ی حساس

| خطر | قید در دیتابیس |
|---|---|
| موجودی منفی | `CHECK (available >= 0)`, `CHECK (pending >= 0)`, `CHECK (withdrawable >= 0 AND withdrawable <= available)` |
| ثبت دوباره‌ی یک عملیات مالی | `UNIQUE (wallet_transactions.idempotency_key)` |
| دو پرداخت با یک تراکنش بلاک‌چین | `UNIQUE (payments.tx_hash)` |
| دو پرداخت با یک Authority | `UNIQUE (payments.gateway, authority)` |
| سفارش / برداشت تکراری | `UNIQUE (orders.idempotency_key)`, `UNIQUE (withdrawals.idempotency_key)` |
| شمارش دوباره‌ی یک عضو | `UNIQUE (campaign_members.campaign_id, telegram_user_id)` |
| عبور از ظرفیت | `CHECK (completed_count >= 0 AND completed_count <= capacity)` + `UPDATE … WHERE completed_count + 1 <= capacity` |
| دو لینک برای یک ناشر | `UNIQUE (campaign_links.campaign_id, publisher_id)`، `UNIQUE (token)` |
| کمیسیون تکراری | `UNIQUE (referral_transactions.referrer_id, source_type, source_id, level)` |
| Replay وب‌هوک | `UNIQUE (telegram_updates.bot_id, update_id)` |
| یک کارت برای دو کاربر | `UNIQUE (bank_accounts.card_hash)` (HMAC، نه خود شماره) |
| حذف سوابق مالی | کلیدهای خارجی `ON DELETE RESTRICT` |

## Ledger

`wallet_transactions` فقط INSERT می‌شود. هر ردیف:

| ستون | معنی |
|---|---|
| `type` | `deposit`, `withdraw`, `purchase`, `refund`, `commission`, `referral`, `transfer`, `campaign_income`, `campaign_expense`, `admin_adjustment` |
| `direction` | `credit` / `debit` |
| `bucket` | `available` یا `pending` |
| `amount` | همیشه مثبت |
| `balance_before`, `balance_after` | موجودی همان `bucket` قبل و بعد |
| `withdrawable_after` | موجودی قابل برداشت پس از این ردیف |
| `reference_type`, `reference_id` | موجودیت مرتبط (`order`, `payment`, `withdrawal`, `campaign`, `campaign_member`, `user`, `admin`) |
| `idempotency_key` | کلید یکتای عملیات |
| `meta` | داده‌ی کمکی (مثلاً `withdrawable_used`, `fee`) |

بازسازی موجودی از Ledger (کنترل حسابرسی):

```sql
SELECT w.user_id, w.available,
       (SELECT COALESCE(SUM(CASE WHEN direction = 'credit' THEN amount ELSE -amount END), 0)
          FROM wallet_transactions t WHERE t.user_id = w.user_id AND t.bucket = 'available') AS ledger_available
FROM wallets w
HAVING w.available <> ledger_available;      -- باید هیچ ردیفی برنگرداند
```

## Migration

```bash
php bin/console migrate            # اجرای Migrationهای جدید
php bin/console migrate --status
php bin/console migrate --rollback # برگرداندن آخرین Batch (روی Production با احتیاط)
```

- فایل‌ها در `database/migrations` به ترتیب نام اجرا می‌شوند و یک شیء با `up(Schema)` و `down(Schema)` برمی‌گردانند.
- **هر تغییر Schema فقط با Migration جدید**؛ فایل‌های اجراشده را ویرایش نکنید.
- `Schema::create()` برای جدول جدید، `Schema::table()` برای افزودن ستون/ایندکس، `Schema::raw()` برای SQL خاص.
- همان Migrationها روی SQLite هم اجرا می‌شوند (تست‌ها).

نمونه:

```php
return new class {
    public function up(Schema $schema): void
    {
        $schema->table('campaigns', static function (Blueprint $t): void {
            $t->string('geo', 8)->nullable();
            $t->index('geo');
        });
    }
    public function down(Schema $schema): void
    {
        $schema->raw('ALTER TABLE campaigns DROP COLUMN geo');
    }
};
```

## نگهداری و پاکسازی

`CleanupJob` (هر ساعت) طبق `config/app.php → retention`:

| داده | مدت نگهداری |
|---|---|
| `telegram_updates` | ۳ روز |
| `notifications` ارسال‌شده / ناموفق | ۶۰ روز |
| `broadcast_recipients` پیام‌های پایان‌یافته | ۳۰ روز |
| `failed_jobs` | ۳۰ روز |
| `risk_events` بررسی‌شده | ۱۸۰ روز |
| فایل‌های لاگ / خروجی | ۳۰ روز / ۲ روز |

**هرگز پاک نمی‌شوند**: `wallet_transactions`, `payments`, `payment_transactions`, `withdrawals`, `referral_transactions`, `audit_logs`, `orders`, `campaigns`.
