# زیرساخت مدیریت پلتفرم — مرحله ششم

وضعیت: پیاده‌سازی حداقل هر ۱۱ تسک تکمیل شده و از روی کد بازبینی شده است. این عنوان به معنی تأیید اجرایی یا آماده‌بودن انتشار نیست. هیچ تست، migration، seeder، درخواست API، ارسال SMS یا اتصال عملیاتی دیتابیس در این نوبت اجرا نشده است. وب‌سرویس و رابط کاربری جدید طبق درخواست مالک ساخته نشده‌اند.

## وضعیت ۱۱ تسک

| ردیف | تسک | خروجی پیاده‌شده |
|---|---|---|
| ۱ | قواعد و مرز دسترسی | قواعد این سند؛ حفاظت هویت مدیر، تفکیک هویت مرکزی و عضویت، عدم خواندن اطلاعات مشتریان |
| ۲ | مدیر اولیه | `InitialPlatformAdminSeeder` و `config/platform_install.php`؛ اجرای مجدد هویت، رمز یا وضعیت را بازنشانی نمی‌کند |
| ۳ | مدیران بعدی | `ManagePlatformUsers.createAdministrator/deactivateAdministrator`؛ چند مدیر کامل، منع غیرفعال‌کردن خود و آخرین مدیر |
| ۴ | مجوزها | کاتالوگ و نقش سیستمی به‌روز؛ مجوز مستقل کاربران، مدیران، کسب‌وکار، آماده‌سازی و ممیزی |
| ۵ | هویت مرکزی | ویرایش چهار ستون نام کوچک/نام خانوادگی/نام کاربری/همراه، مسدودسازی و افزایش auth_epoch؛ عضویت‌های کسب‌وکار تغییر نمی‌کنند |
| ۶ | فراموشی رمز | `AccountRecovery` با چالش مستقل، OTP، رمز جدید و تأیید آن، سقف تلاش، ابطال نشست‌های پیشین و کدهای ورود قبلی |
| ۷ | کسب‌وکار و مالک | ثبت تکرارپذیر، استفاده از هویت موجود یا ایجاد هویت pending، پذیرش مالک، نقش و دسترسی صریح شعبه اصلی در تراکنش |
| ۸ | مشخصات و وضعیت | `BusinessLifecycle` برای مشخصات پایه، تغییر زیردامنه، فعال‌سازی/تعلیق/بستن؛ alias دائمی آدرس قبلی؛ لوگوی موجود متصل به مدیا و ممیزی |
| ۹ | مقصد دیتابیس | ثبت مقصد از تنظیمات امن، تخصیص مشترک پیش‌فرض یا اختصاصی آماده، پذیرش و بررسی مقصد بدون اجرای migration برای هر کسب‌وکار |
| ۱۰ | ممیزی | عامل، هدف، علت، فیلدها و snapshot مجاز قبل/بعد؛ ثبت شکست عمومی بدون متن استثنا و اطلاعات محرمانه |
| ۱۱ | آماده‌سازی API | سرویس‌های مستقل از کنترلر، قراردادهای Identity/SMS/AccessControl، فهرست ورودی و خروجی این سند؛ بازبینی ایستای کد و قالب‌بندی |

## قواعد قطعی

- مدیر اولیه با سیدر ساخته می‌شود و مدیران با نام کاربری و رمز عبور وارد می‌شوند. سیدر آزمایشی قبلی، جایگزین سیدر رسمی نیست.
- مدیر موجود مدیر جدید می‌سازد؛ مدیر جدید می‌تواند حساب مدیر قبلی را غیرفعال کند. تغییر یا مسدودسازی حساب مدیر از مسیر عمومی کاربران ممنوع است. غیرفعال‌سازی خود ممنوع است.
- ویرایش هویت مرکزی فقط در اختیار مدیر پلتفرم است. مالک یا مدیر کسب‌وکار چنین اختیاری ندارد. نام عضویت کسب‌وکار مستقل است.
- اطلاعات مشتریان از دیتابیس عملیاتی در این سرویس‌ها خوانده نمی‌شود. در ماژول‌های آینده، عضویت BUSINESS برای کارکنان است؛ مشتری صرفاً به دلیل مشتری‌بودن چنین عضویتی نمی‌گیرد.
- هر کسب‌وکار یک مالک دارد و یک فرد می‌تواند مالک چند کسب‌وکار باشد. ایجاد کسب‌وکار، مالکیت را خودکار تأیید نمی‌کند.
- مشخصات پایه قابل ویرایش فعلی: نام، توضیحات، تلفن و ایمیل. مالک، زیردامنه، وضعیت و لوگو هرکدام مسیر کنترل‌شده جدا دارند؛ ورودی نامعتبر یا فیلد خارج از فهرست پذیرفته نمی‌شود.

## قرارداد سرویس‌ها برای وب‌سرویس‌های آینده

شناسه عامل و `authenticatedUserId` فقط از نشست معتبر سمت سرور گرفته شوند؛ پذیرش آن‌ها از بدنه درخواست عمومی ممنوع است. `PlatformIdentity` صرفاً حامل شناسه است و جای middleware اعتبار نشست را نمی‌گیرد. کنترلرهای آینده موظف به کنترل نشست، محدودیت نرخ و پاسخ `no-store` هستند.

| سرویس/متد | ورودی اصلی | خروجی/رفتار |
|---|---|---|
| `ManagePlatformUsers.update` | عامل، UUID کاربر، فیلدهای مجاز، رمز مجدد، کد علت | تغییر مرکزی و افزایش epoch؛ همراه جدید تأییدنشده می‌شود |
| `ManagePlatformUsers.block` | عامل، UUID کاربر، رمز مجدد، علت | suspended مرکزی؛ قطع اعتبار تمام نشست‌های قدیمی |
| `ManagePlatformUsers.createAdministrator` | عامل، مشخصات حساب جدید و رمز، رمز عامل، علت | UUID مدیر جدید؛ نقش کامل فعلی پلتفرم |
| `ManagePlatformUsers.deactivateAdministrator` | عامل، UUID مدیر هدف، رمز عامل، علت | غیرفعال‌سازی حساب و عضویت پلتفرم |
| `ReadPlatformDirectory` | عامل، cursor و limit | کاربران، کسب‌وکارها، جزئیات، اعضا، شعب و دسترسی شعب؛ صفحات تا ۱۰۰ ردیف در متدهای صفحه‌بندی‌شده |
| `ReadPlatformDirectory.auditEvents` | عامل، afterId، limit | فقط ستون‌های مجاز ممیزی با مجوز platform.audit.view |
| `ReadPlatformDirectory.accountCodeMessages` | عامل، afterUid، limit | نتیجه ارسال کدهای مرکزی؛ بدون کد، متن پیام یا همراه |
| `ManageBusinesses.create` | عامل، requestUid، مشخصات و یک شناسه مالک، رمز عامل | UUID کسب‌وکار در انتظار؛ ثبت دوباره همان درخواست خروجی قبلی را می‌دهد |
| `ManageBusinesses.acceptOwnership` | شناسه هویت احرازشده مالک و UUID کسب‌وکار | پذیرش، فعال‌سازی عضویت، نقش و دسترسی شعبه در یک تراکنش |
| `PrepareBusiness.registerStore` | connectionRef تنظیم‌شده، مالک مقصد اختصاصی یا null | UUID مقصد؛ اعتبارسنجی تنظیمات بدون ساخت دیتابیس |
| `PrepareBusiness.allocate` | کسب‌وکار، UUID مقصد یا null، رمز عامل و علت | null یعنی نخستین مقصد مشترک فعال؛ انتقال مقصد موجود ممنوع |
| `PrepareBusiness.prepare` | عامل، UUID کسب‌وکار، رمز و علت | بررسی مقصد از پیش نصب‌شده، پذیرش tenant و ثبت READY یا کد عمومی FAILED |
| `BusinessLifecycle.update` | عامل، UUID کسب‌وکار، فیلدهای پایه، رمز و علت | تغییر مشخصات و ممیزی |
| `BusinessLifecycle.renameSubdomain` | عامل، UUID کسب‌وکار، زیردامنه، رمز و علت | رزرو دائمی آدرس قبلی و فعال‌شدن آدرس جدید |
| `BusinessLifecycle.changeStatus` | عامل، UUID کسب‌وکار، وضعیت، رمز و علت | فقط انتقال وضعیت مجاز و با پیش‌شرط |

شناسه مالک در create فقط یکی از `owner_uid` یا `owner_mobile` است. همراه باید E.164 ایران باشد. هویت جدید pending و بدون رمز ساخته می‌شود؛ نام ثبت‌شده مالک در عضویت ذخیره می‌شود. اطلاعات هویت قبلی در اثر ایجاد کسب‌وکار بازنویسی نمی‌شود.

`request_uid` باید UUID جدید برای هر عملیات ثبت باشد. استفاده دوباره از آن با ورودی متفاوت خطای `REQUEST_KEY_REUSED` دارد. تکرار عملیات در برابر ناموجودبودن مجوز یا رمز صحیح استثنا نیست و دوباره کنترل می‌شود.

## فراموشی رمز و مالک جدید

قرارداد عمومی `Modules\Identity\PublicContracts\AccountRecovery`:

- `requestReset(mobile, ip)`: پاسخ عمومی `challenge_id / expires_in / message`؛ پاسخ وجود حساب را صریحاً اعلام نمی‌کند. برای حساب مسدود ارسال مؤثر انجام نمی‌شود.
- `reset(challengeUid, code, password, confirmation)`: رمز جدید و تکرار آن لازم است؛ رمز قبلی لازم نیست. چالش فقط برای RESET_PASSWORD مصرف می‌شود. حساب مسدود فعال نمی‌شود.
- `requestOwnership(businessUid, mobile, ip)`: فقط برای کسب‌وکار در انتظار؛ ارسال تنها به همراه مالک همان کسب‌وکار انجام می‌شود و پاسخ عمومی است.
- `acceptOwnership(challengeUid, code, password, confirmation)`: برای هویت جدید رمز تعیین می‌کند؛ برای هویت دارای رمز، همان رمز موجود باید تأیید شود و این مسیر آن را بازنشانی نمی‌کند. سپس پذیرش مالک در همان تراکنش انجام می‌شود.

کدها ۵ دقیقه معتبرند و حداکثر ۵ تلاش دارند. صدور با محدودیت مشترک mobile/IP ورود و بازیابی هماهنگ است. کد خام در جدول، صف یا سابقه ذخیره نمی‌شود. بررسی اعتبار موبایل، epoch و وضعیت حساب هنگام مصرف تکرار می‌شود. رمز جدید، مصرف کد، تغییر هویت و پذیرش مالکیت در تراکنش هماهنگ‌اند.

`AccountCodeSender` از سرویس sms.ir و قالب OTP موجود در تنظیمات امن استفاده می‌کند. جدول مستقل `sms_identity_messages` فقط نتیجه و شناسه سرویس را نگه می‌دارد. هزینه کد هویت مرکزی با پلتفرم است؛ از کیف پول یک کسب‌وکار تصادفی کم نمی‌شود. سقف ساعتی پلتفرم در `sms.account_codes_per_hour` است. نتیجه UNKNOWN یا SENDING خودکار دوباره ارسال نمی‌شود؛ درخواست مجدد، چالش جدید و محدودیت ارسال دارد. به علت عدم وجود API جدید، تنظیمات پستمن این متدها در این مرحله اضافه نشده است.

## نصب و آماده‌سازی محل داده

۱. مهاجرت‌های مرکزی و سیدر کاتالوگ باید توسط مسئول نصب اجرا شوند؛ در این کار اجرا نشده‌اند.
۲. متغیرهای `INITIAL_PLATFORM_ADMIN_USERNAME`، `INITIAL_PLATFORM_ADMIN_MOBILE` و `INITIAL_PLATFORM_ADMIN_PASSWORD` در تنظیمات امن همان محیط وارد شوند؛ نام کوچک و خانوادگی اختیاری در تنظیمات نصب قابل تعیین‌اند. رمز در آرگومان فرمان یا گزارش نوشته نشود.
۳. سیدر رسمی: `php artisan db:seed --class=InitialPlatformAdminSeeder`. در production گزینه استاندارد `--force` لازم است. این فرمان در این نوبت اجرا نشده است.
۴. برای OAuth کلید و personal access client مطابق دستور نصب موجود در `ARCHITECTURE_V2.fa.md` لازم‌اند. با اجرای دوباره نصب، کلیدها بازنویسی نشوند.
۵. DBA دیتابیس اختصاصی یا مشترک و نقش‌های تفکیک‌شده را آماده می‌کند. تنظیمات امن اتصال در `tenant_databases.stores` قرار می‌گیرند؛ ورودی کاربر فقط connectionRef است.
۶. مقصد از طریق registerStore ثبت می‌شود. نصب/ارتقای schema از مسیر موجود OperationalInstaller یک‌بار برای هر مقصد انجام می‌شود، نه هنگام ساخت هر کسب‌وکار.
۷. allocate مقصد کسب‌وکار در انتظار را تعیین می‌کند. prepare فقط schema و marker و نقش‌ها را بررسی و tenant_businesses را به‌شکل تکرارپذیر ثبت می‌کند؛ هیچ migration اجرا نمی‌کند.
۸. اگر ثبت remote موفق و تراکنش مرکزی ناموفق شود، ردیف پذیرش remote به‌تنهایی کسب‌وکار را فعال نمی‌کند. تکرار prepare ادامه امن عملیات است. انتقال داده یا جایگزینی مقصد موجود جزو این مرحله نیست.

## وضعیت و تعلیق

- PENDING_ACTIVATION → ACTIVE یا CLOSED.
- ACTIVE → SUSPENDED یا CLOSED.
- SUSPENDED → ACTIVE یا CLOSED.
- CLOSED مسیر بازگشت عمومی ندارد؛ حذف فیزیکی نیز انجام نمی‌شود.

فعال‌سازی نیازمند پذیرش مالک، هویت فعال با همراه تأییدشده، عضویت و نقش مالک، دسترسی صریح شعبه، آماده‌سازی READY و تطابق و سلامت مقصد فعلی است. هنگام فعال‌سازی شعبه اصلی فعال می‌شود.

برای SUSPENDED، درخواست امن HTTP می‌تواند به مسیر عملیاتی مجاز وارد شود؛ پست‌گرس تراکنش آن را READ ONLY می‌کند. درخواست نوشتنی عملیاتی و کارهای صف با TenantRuntime پیش‌فرض رد می‌شوند. دسترسی خواندن همچنان به مجوز و حق استفاده از ماژول وابسته است. خدمات محدود مرکزی مثل صورتحساب طبق سیاست قبلی باقی می‌مانند.

ماژول‌های آینده باید برای HTTP از business.context و permission و برای صف از UseTenantDatabase استفاده کنند. قبل از اثر جانبی خارجی در عملیات محصول، `TenantRuntime.assertWritable()` لازم است؛ `afterCommit()` نیز آن را اعمال می‌کند. تغییر داده از GET و اجرای عملیات عملیاتی خارج از این مرزها مجاز نیست. تراکنش درحال‌اجرایی که پیش از تعلیق شروع شده، با این مکانیزم به‌اجبار قطع نمی‌شود؛ درخواست و کار جدید سیاست جدید را می‌گیرد.

آدرس‌های پیشین برای همان کسب‌وکار resolve می‌شوند و hostname خروجی canonical است؛ هیچ آدرس پیشین به کسب‌وکار دیگر واگذار نمی‌شود. این مرحله redirect رابط Next.js ایجاد نمی‌کند.

## ممیزی، محرمانگی و خطاها

عامل، هدف، زمان، کد علت و نتیجه در security_audit_events ثبت می‌شوند. snapshot فقط فیلدهای مجاز دارد؛ رمز، هش رمز، OTP، API key و اطلاعات اتصال در آن مجاز نیستند. شکست‌های مدیریتی با هدف عمومی و کد PLATFORM_OPERATION_FAILED و بدون متن استثنا ثبت می‌شوند؛ شکست ثبت ممیزی جای خطای اصلی را نمی‌گیرد. برای ثبت موفق ممیزی، شکست SQL باعث rollback تغییر اصلی می‌شود.

کدهای داخلی مهم: INITIAL_ADMIN_IDENTITY_CONFLICT، ADMIN_IDENTITY_PROTECTED، CANNOT_DEACTIVATE_SELF، LAST_ADMIN_PROTECTED، IDENTITY_VALUE_UNAVAILABLE، REQUEST_KEY_REUSED، SUBDOMAIN_UNAVAILABLE، OWNER_UNAVAILABLE، OWNER_ACCESS_NOT_READY، OWNER_ROLE_REQUIRED، DATA_STORE_UNAVAILABLE، PLACEMENT_ALREADY_ALLOCATED، BUSINESS_PREPARATION_FAILED، BUSINESS_NOT_READY، PREPARED_STORE_CHANGED و ACCOUNT_VERIFICATION_FAILED.

این‌ها کدهای لایه کاربردند؛ تبدیل دقیق آن‌ها به قرارداد پاسخ API در زمان تعریف وب‌سرویس‌ها انجام می‌شود. رندر فعلی HTTP برخی خطاهای abort را به کدهای عمومی تبدیل می‌کند.

## حدود نتیجه و بررسی انجام‌شده

سیدر، قراردادها، bindingها، manifest، روابط، ترتیب قفل و تراکنش، شرایط ابطال کد و نشست، منع نوشتن در تعلیق و فهرست مجوزها از روی کد بازبینی شدند. قالب‌بندی کد انجام شد. اجرای واقعی migration، تست و سناریوی اتصال به PostgreSQL/sms.ir و آزمون پذیرش انتشار انجام نشده است و نباید انجام‌شده تلقی شود.

این مرحله حداقل زیرساخت مدیریت پلتفرم است؛ ثبت عمومی محصول، مدیریت پلن آزمایشی، انتقال مالکیت، انتقال دیتابیس، بستن حساب با تسویه مالی و ابزارهای گزارش قدیمی مراحل محصول بعدی‌اند و از فهرست حفظ قابلیت‌های نسخه قبلی حذف نشده‌اند.


اصلاح بازبینی: آماده‌سازی با قفل نشست مختص کسب‌وکار و تراکنش‌های کوتاه انجام می‌شود. مالک کسب‌وکار قدیمی فعال یا معلق نیز می‌تواند پذیرش واقعی انجام دهد؛ سابقه صرفاً پس از این اقدام ایجاد می‌شود و سپس prepare مقصد و دسترسی‌ها را بررسی می‌کند. فهرست‌ها قفل سراسری تغییر ندارند. شمارش خطای رمز از اتصال مستقل کش استفاده می‌کند. نصب مایگریشن repair_identity_and_sms_boundaries پیش از کد جدید الزامی است؛ اجرا و تست نشده است.

اصلاح بازبینی آسترا: شناسه preparation_attempt_uid برای هر تلاش آماده‌سازی در دیتابیس مرکزی ثبت می‌شود. ثبت READY یا FAILED فقط با تطبیق شناسه همان تلاش و وضعیت PREPARING انجام می‌شود؛ نتیجه اجرای قدیمی نمی‌تواند تلاش جدید را بازنویسی کند. مهاجرت 2026_11_03_130000_add_preparation_attempt_to_business_onboarding.php پیش از استقرار کد جدید باید از مسیر نصب مرکزی اعمال شود؛ در این تحویل اجرا نشده است. پذیرش مالکیت برای عضو ACTIVE تاریخ عضویت و تغییر وضعیت را حفظ می‌کند. برای عضو PENDING، تاریخ عضویت قبلی حفظ و در صورت خالی‌بودن مقداردهی می‌شود. تستی اجرا نشده است.
