<div dir="rtl">

# راهنمای استقرار MiniCRM

این سند خروجی فاز ۸ (Security Hardening، Performance، Browser Test، Deployment Docs) است.

## ۱. پیش‌نیازها

- PHP 8.2+ با پسوندهای: `pdo_mysql`, `bcmath`, `mbstring`, `intl`, `gd` یا `imagick` (برای DomPDF), `zip`
- MySQL 8+ یا MariaDB 10.6+ (یا PostgreSQL — کد Portable است، ADR بند ۲)
- Composer 2.x، Node 20+ / npm
- دسترسی نوشتن به `storage/` و `bootstrap/cache/`

## ۲. تنظیم Environment

```bash
cp .env.example .env
php artisan key:generate
```

مقادیر حیاتی که باید در `.env` تولیدی تنظیم شوند:

| متغیر | نکته |
|---|---|
| `APP_ENV` | `production` |
| `APP_DEBUG` | `false` — هرگز در Production روشن نماند (نشت اطلاعات Stack Trace) |
| `APP_URL` | آدرس واقعی HTTPS |
| `DB_*` | اتصال دیتابیس Production |
| `SESSION_SECURE_COOKIE` | `true` وقتی سرویس پشت HTTPS است (پیش‌فرض `.env.example` روی `false` برای توسعه محلی HTTP است) |
| `SESSION_DOMAIN` | دامنه واقعی، نه `null`، اگر چند ساب‌دامنه Session مشترک دارند |
| `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` | برای فعال‌سازی Push واقعی — بخش ۷ |
| `MAIL_*` | سرویس واقعی ایمیل (نه `log`) |

اگر پشت Reverse Proxy (Nginx/Load Balancer با TLS Termination) قرار دارد، `App\Http\Middleware\TrustProxies` (پیش‌فرض لاراول) باید IP پروکسی را بشناسد — یا `TRUSTED_PROXIES=*` را در صورت اطمینان از شبکه داخلی تنظیم کنید.

## ۳. نصب و Build

```bash
composer install --no-dev --optimize-autoloader
npm ci && npm run build
```

## ۴. دیتابیس

**هرگز از `migrate:fresh` در Production استفاده نشود** — رکورد Tenant واقعی پاک می‌شود.

```bash
php artisan migrate --force
```

اجرای اولیه Seeder فقط برای محیط Demo/Staging مناسب است، نه Production (کاربر و رمز عبور ثابت می‌سازد):

```bash
php artisan db:seed --force   # فقط Staging/Demo
```

موتور جدول‌ها باید InnoDB باشد (نه MyISAM) — `config/database.php` این را برای MySQL/MariaDB به‌صورت صریح تنظیم می‌کند (ADR بند ۲)، اما اگر سرور دیتابیس هدف `default_storage_engine` متفاوتی دارد دوباره بررسی شود.

## ۵. Queue Worker

`QUEUE_CONNECTION=database` است (بدون Redis، ADR بند ۲) — کارهای Queue (تولید PDF پیش‌فاکتور، خروجی Excel گزارش‌ها) بدون یک Worker در حال اجرا هرگز پردازش نمی‌شوند. با Supervisor:

```ini
[program:minicrm-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/minicrm/artisan queue:work database --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
numprocs=2
redirect_stderr=true
stdout_logfile=/path/to/minicrm/storage/logs/worker.log
```

**اتوماسیون (کمپین و Workflow):** اجرای هر Execution روی صف جداگانهٔ `automation` انجام می‌شود. Worker باید این صف را هم بخواند: `--queue=automation,default`. اگر Worker دائمی (Supervisor) ندارید و فقط Cron دارید، هر دقیقه یک Worker کوتاه اجرا کنید:

```cron
* * * * * cd /path/to/minicrm && php artisan queue:work database --queue=automation,default --stop-when-empty --max-time=55 >> /dev/null 2>&1
```

مقدار `retry_after` اتصال صف (پیش‌فرض ۹۰ ثانیه) باید بیشتر از `timeout` کار (۶۰ ثانیه) بماند؛ وگرنه یک Execution ممکن است هم‌زمان به دو Worker برسد. خود Jobهای اتوماسیون عمداً `tries = 1` دارند: تلاش مجدد ارسال پیامک فقط با سیاست ثبت‌شدهٔ خود Workflow انجام می‌شود، نه با Retry صف.

## ۶. Scheduler

فرمان `exports:expire` (پاک‌سازی روزانه خروجی‌های منقضی، فاز ۷) باید توسط Laravel Scheduler اجرا شود — یک ورودی Cron واحد کافی است:

```cron
* * * * * cd /path/to/minicrm && php artisan schedule:run >> /dev/null 2>&1
```

فرمان `automation:tick` (هر دقیقه) Executionهای سررسیدشده را Claim و به صف می‌دهد و Executionهای رهاشده توسط Workerِ مرده را برمی‌گرداند؛ خودش هیچ پیامکی ارسال نمی‌کند. خروجی آن در `storage/logs/schedule.log` است.

افزودن دستهٔ بزرگ تارگت (بیش از ۳۰۰) به یک کمپین هم به‌صورت Job روی همین صف `automation` انجام می‌شود؛ بدون Worker، صفحهٔ «افزودن تارگت» تا ابد در حالت «در حال افزودن» می‌ماند. پس از استقرار این نسخه، علاوه بر `php artisan migrate`، `npm run build` هم لازم است (کلاس‌های Tailwind صفحه‌های کمپین).

**پایش خودکار:** اگر Cron (`schedule:run` هر دقیقه) یا Worker صف `automation` خاموش باشد، صفحهٔ کمپین‌ها هشدار می‌دهد («زمان‌بند اجرا نمی‌شود» / «صف پردازش نمی‌شود»)؛ ضربان زمان‌بند در Cache نگه داشته می‌شود، پس Cache باید بین درخواست وب و Cron مشترک باشد (درایور `file` یا `database`، نه `array`).

**قطع‌شدن کانال پیامک:** اگر اعتبار پنل تمام شود یا پیامک خاموش باشد، ارسال آن Workspace ۳۰ دقیقه متوقف می‌شود، مالک اعلان می‌گیرد و پس از رفع مشکل ارسال خودکار (یا با دکمهٔ «مشکل برطرف شد؛ ادامه») برمی‌گردد.

**گزارش تحویل (Webhook):** آدرس اختصاصی هر Workspace در «تنظیمات ← پیامک» (پس از روشن‌کردن اتوماسیون) نمایش داده می‌شود: `POST /api/webhooks/sms/generic/{token}`. این فرمت خنثای خود سیستم است؛ فرمت اختصاصی ملی‌پیامک هنوز با پنل واقعی راستی‌آزمایی نشده و پشتیبانی نمی‌شود.

## ۷. Web Push (اختیاری)

برای فعال‌سازی ارسال واقعی Push (نه فقط اعلان داخل‌برنامه‌ای، فاز ۷):

```bash
php artisan tinker --execute="print_r(Minishlink\WebPush\VAPID::createVapidKeys());"
```

مقادیر `publicKey`/`privateKey` را در `VAPID_PUBLIC_KEY`/`VAPID_PRIVATE_KEY` قرار دهید. تا وقتی این دو مقدار خالی باشند، `NotificationService` فقط اعلان داخل‌برنامه‌ای می‌سازد و بی‌صدا از ارسال Push صرف‌نظر می‌کند (بدون خطا).

## ۸. پشتیبان‌گیری (Backup)

سند مرجع «Backup روزانه و تست Restore» را الزامی می‌داند. این پروژه پکیجی برای Backup نصب نکرده (بدون وابستگی اضافه بدون کاربرد تأییدشده)؛ حداقل راه‌حل قابل‌اجرا:

```cron
0 3 * * * mysqldump --single-transaction -u USER -pPASSWORD minicrm | gzip > /backups/minicrm-$(date +\%F).sql.gz
```

- فایل‌های خصوصی روی دیسک `local` (پیوست‌ها، PDF پیش‌فاکتور، خروجی‌های Excel در `storage/app/private`) باید جدا از دیتابیس هم پشتیبان‌گیری شوند.
- بازه نگه‌داری (مثلاً ۳۰ روز) و محل ذخیره خارج از سرور اصلی (Off-site) تعیین و مستند شود.
- **تست Restore را واقعاً روی یک محیط جدا اجرا کنید** — پشتیبانی که هرگز Restore نشده، تأییدشده نیست.

## ۹. چک‌لیست امنیتی پیش از استقرار

- [ ] `APP_DEBUG=false`
- [ ] `SESSION_SECURE_COOKIE=true` (اگر HTTPS)
- [ ] HTTPS با گواهی معتبر و Redirect خودکار HTTP→HTTPS در Web Server
- [ ] `php artisan config:cache && php artisan route:cache && php artisan view:cache`
- [ ] Queue Worker و Scheduler فعال و پایدار (بخش‌های ۵ و ۶)
- [ ] Backup روزانه تنظیم و حداقل یک‌بار Restore تست‌شده (بخش ۸)
- [ ] کلیدهای VAPID تولید و تنظیم شده (اگر Push لازم است)
- [ ] رمز عبور کاربر seeded (`owner@example.com` و مشابه) در Production استفاده نشده — این حساب‌ها فقط برای Demo/Staging هستند

## ۱۰. تست‌های مرورگری (Laravel Dusk)

Dusk (فاز ۸) در `tests/Browser` نصب و پیکربندی شده و پوشش می‌دهد:

- ورود کاربر از فرم واقعی و رسیدن به داشبورد.
- چیدمان راست‌به‌چپ (`dir="rtl"` و Sidebar در نیمه راست صفحه).
- باز شدن کشوی «ثبت سریع» آفلاین و صف‌شدن یک Draft مشتری — تست بازگشتی برای دو باگ واقعی که در فاز ۷ حین تست دستی مرورگر پیدا و رفع شدند (بند ۱۱.۷ در `architecture-decisions.md`).

اجرای این تست‌ها نیازمند یک دیتابیس MySQL **جدا** از دیتابیس توسعه اصلی است (`.env.dusk.local` → `minicrm_dusk`)، چون تست‌ها از Trait `DatabaseMigrations` استفاده می‌کنند که پیش از هر کلاس تست، دیتابیس را کاملاً از نو Migrate می‌کند — روی دیتابیس اصلی این یعنی از دست رفتن داده Demo. روش اجرا (دو ترمینال جدا):

```bash
# ترمینال ۱ — یک‌بار دیتابیس اختصاصی را بسازید، سپس:
cp .env .env.backup   # نگه‌داشتن env فعلی
cp .env.dusk.local .env
php artisan serve --port=8000

# ترمینال ۲
php artisan dusk

# پس از پایان تست‌ها، در ترمینال ۱ (Ctrl+C روی serve سپس):
cp .env.backup .env
```

`php artisan dusk` خودش پیش و پس از اجرا `.env` را با `.env.dusk.local` جابه‌جا می‌کند؛ نکته بالا فقط برای همسو نگه‌داشتن سروری است که مرورگر واقعاً به آن وصل می‌شود (چون آن سرور یک پردازه جدا و از پیش در حال اجراست، نه بخشی از دستور `dusk`).

</div>
