# قراردادهای ارتباطی سرویس‌ها (Service Contracts) ## ۱. قوانین بنیادی 1. **ارتباط مستقیم بین دیتابیس سرویس‌ها ممنوع است.** هیچ سرویسی نباید به دیتابیس سرویس دیگر کوئری بزند. 2. هر سرویس فقط دیتابیس خودش را می‌شناسد (Database-per-service). 3. ارتباط بین سرویس‌ها فقط از این چهار راه: - **REST API** (همزمان) - **Webhook** (اعلان به بیرون) - **Async Event** (message bus) - **Outbox/Inbox Pattern** (تحویل قابل اعتماد رویداد) 4. همه درخواست‌های tenant-aware باید `tenant_id` را حمل کنند (هدر `X-Tenant-ID` یا داخل توکن/رویداد). ## ۲. احراز هویت بین سرویس‌ها - سرویس‌ها با **Internal Service Token** یکدیگر را احراز می‌کنند. - توکن به‌صورت hash در `internal_service_tokens` ذخیره می‌شود (خود توکن ذخیره نمی‌شود). - هر توکن دارای `scopes` است؛ سرویس فراخوان باید scope لازم را داشته باشد. - کاربر نهایی با JWT کیکلوک احراز می‌شود. ## ۳. قرارداد رویداد (Event Envelope) همه رویدادها با ساختار مشترک `shared.events.EventEnvelope` منتشر می‌شوند: ```json { "event_id": "uuid", "event_type": "tenant.created", "aggregate_type": "tenant", "aggregate_id": "uuid", "tenant_id": "uuid|null", "source_service": "core-service", "payload": { }, "occurred_at": "ISO-8601" } ``` ### جریان Outbox → Inbox 1. سرویس مبدأ رویداد را در `outbox_events` (در همان تراکنش) ذخیره می‌کند. 2. worker رویدادهای `pending` را منتشر و `processed` می‌کند. 3. سرویس مقصد رویداد را در `inbox_events` ثبت می‌کند (بر اساس `event_id` برای idempotency) و سپس پردازش می‌کند. ### رویدادهای شناخته‌شده Core (نمونه) | event_type | aggregate | توضیح | | --- | --- | --- | | tenant.created | tenant | ساخت مستأجر جدید | | tenant.suspended | tenant | تعلیق مستأجر | | tenant.activated | tenant | فعال‌سازی مستأجر | | domain.created | domain | افزودن دامنه | | subscription.created | subscription | ایجاد اشتراک | | subscription.updated | subscription | تغییر اشتراک | | feature_access.changed | feature_access | تغییر دسترسی قابلیت | ## ۴. قرارداد Entitlement (بررسی دسترسی) سرویس‌های دیگر پیش از اجرای یک قابلیت، دسترسی را از Core استعلام می‌کنند: ``` POST /api/v1/tenants/{tenant_id}/features/check { "feature_key": "accounting.invoice.create" } → { "tenant_id": "...", "feature_key": "...", "has_access": true, "reason": "plan_enabled" } ``` قرارداد نام‌گذاری `feature_key`: `{service_key}.{resource}.{action}` مثال: `crm.lead.create`, `ecommerce.product.create`. ## ۵. APIهای Core Platform (فاز ۱) | گروه | متد و مسیر | | --- | --- | | Health | `GET /health` | | Tenants | `POST/GET /api/v1/tenants`، `GET/PATCH /api/v1/tenants/{id}`، `POST .../suspend`، `POST .../activate` | | Domains | `POST/GET /api/v1/tenants/{id}/domains`، `POST /api/v1/domains/resolve` | | Plans & Features | `POST/GET /api/v1/plans`، `POST/GET /api/v1/features`، `POST /api/v1/plans/{id}/features` | | Subscription | `POST/GET /api/v1/tenants/{id}/subscription`، `POST /api/v1/tenants/{id}/features/check` | | Service Registry | `POST/GET /api/v1/services`، `PATCH /api/v1/services/{id}/status` | ## ۶. قالب پاسخ استاندارد - خطاها با قالب `shared.responses.ErrorResponse` برگردانده می‌شوند: ```json { "success": false, "error": { "code": "not_found", "message": "...", "details": null } } ``` - لیست‌ها با قالب صفحه‌بندی `Page` (شامل `items` و `meta`). ## ۷. Onboarding و Tenant Context (فاز ۴ — Tenant Onboarding و Workspace Activation) > در بریف پروژه این مجموعه API با عنوان «Phase 3» شناخته می‌شود؛ در > شماره‌گذاری داخلی مستندات فاز ۴ است (نگاه کنید به `progress.md`). همهٔ endpointهای این بخش زیر `API_V1_PREFIX` (پیش‌فرض `/api/v1`) هستند و به احراز هویت نیاز دارند (`Authorization: Bearer ` — چه JWT محلی OTP و چه JWT کیکلوک SSO؛ هر دو با `get_current_core_user` resolve می‌شوند). ### `GET /api/v1/me` وضعیت کاربر جاری، عضویت‌ها و نیاز به onboarding. **پاسخ ۲۰۰:** ```json { "user_id": "uuid", "mobile": "09xxxxxxxxx", "email": null, "platform_role": "user", "onboarding_required": true, "current_tenant_id": null, "memberships": [ { "tenant_id": "uuid", "tenant_name": "...", "tenant_slug": "...", "tenant_status": "active", "onboarding_completed": true, "role": "tenant_owner", "status": "active", "is_owner": true } ] } ``` `onboarding_required=true` اگر کاربر هیچ عضویتی نداشته باشد یا tenant جاری هنوز `onboarding_completed=false` باشد. خطاها: `401 unauthorized` (بدون توکن)، `403 forbidden` (JWT کیکلوک بدون لینک به رکورد Core — نگاه کنید به `architecture.md`). ### `GET /api/v1/me/tenants` آرایه‌ای از `MembershipSummary` (همان ساختار `memberships` بالا) برای همهٔ tenantهای در دسترس کاربر جاری. ### `POST /api/v1/onboarding/tenant` ساخت tenant جدید توسط کاربر جاری (بدون نیاز به عضویت قبلی). **بدنه درخواست:** ```json { "business_name": "کافه تربت", "slug": "cafe-torbat", "business_type": "cafe", "default_locale": "fa-IR", "timezone": "Asia/Tehran" } ``` `business_name` و `slug` الزامی؛ بقیه اختیاری (پیش‌فرض‌ها بالا). `slug` باید فقط حروف کوچک انگلیسی/اعداد/خط‌تیره باشد. **رفتار:** ساخت tenant با `status=pending_activation` → ساخت عضویت `tenant_owner`/`is_owner=true` برای کاربر جاری → اتصال پلن پیش‌فرض `FREE` با `tenant_subscriptions.status=active` → (در صورت تنظیم `PLATFORM_BASE_DOMAIN`) ساخت زیردامنهٔ `{slug}.{base_domain}` با `verification_status=verified` → اگر کاربر `current_tenant_id` نداشت، همین tenant به‌عنوان جاری تنظیم می‌شود. **پاسخ ۲۰۱:** شیء `TenantContextRead` (نگاه کنید پایین). خطاها: `409 slug_taken` (slug تکراری در کل پلتفرم)، `401`/`403` مشابه بالا. ### `PATCH /api/v1/onboarding/tenant/{tenant_id}/branding` ```json { "primary_color": "#112233", "secondary_color": "#445566", "logo_url": null, "favicon_url": null } ``` همهٔ فیلدها اختیاری‌اند؛ فقط مقادیر ارسال‌شده به‌روزرسانی می‌شوند. نیازمند نقش `tenant_owner`/`tenant_admin` روی tenant مقصد (یا `platform_admin`). پاسخ ۲۰۰: `TenantContextRead`. خطا: `403 forbidden` (بدون نقش کافی). ### `PATCH /api/v1/onboarding/tenant/{tenant_id}/domain` ```json { "custom_domain": "cafe.example.com" } ``` اگر `custom_domain` ارسال شود، رکورد `domains` جدید با `domain_type=custom_domain`، `is_primary=false`، `verification_status=pending` ساخته می‌شود. پاسخ ۲۰۰: `TenantContextRead`. خطاها: `409 domain_taken` (دامنه تکراری در کل پلتفرم)، `403 forbidden`. ### `POST /api/v1/onboarding/tenant/{tenant_id}/complete` بدون بدنه. اعتبارسنجی نهایی: نام/slug موجود باشد و حداقل یک owner فعال داشته باشد؛ در صورت موفقیت `onboarding_completed=true` و `status=active` می‌شود. پاسخ ۲۰۰: `TenantContextRead`. خطاها: `422 onboarding_incomplete` (نام/slug ناقص)، `422 owner_required` (بدون owner فعال)، `403 forbidden` (بدون نقش کافی). ### `GET /api/v1/tenant/current` tenant جاری کاربر را برمی‌گرداند (بر اساس `current_tenant_id` یا اولین عضویت در نبود آن). پاسخ ۲۰۰: `TenantContextRead`. خطا: `404 not_found` (کاربر هیچ عضویتی ندارد). ### `POST /api/v1/tenant/switch` ```json { "tenant_id": "uuid" } ``` `current_tenant_id` کاربر را به این tenant تغییر می‌دهد (فقط اگر عضویت `active` روی آن داشته باشد). پاسخ ۲۰۰: `TenantContextRead`. خطا: `403 forbidden` (عضو نیست یا عضویت غیرفعال). ### ساختار مشترک `TenantContextRead` ```json { "tenant": { "id": "...", "name": "...", "slug": "...", "status": "active", "business_type": null, "default_locale": "fa-IR", "timezone": "Asia/Tehran", "primary_color": "#0284c7", "secondary_color": "#0f172a", "logo_url": null, "favicon_url": null, "onboarding_completed": true, "created_at": "...", "updated_at": "..." }, "role": "tenant_owner", "is_owner": true, "plan_code": "FREE", "plan_name": "رایگان", "subscription_status": "active", "domains": [ { "id": "...", "tenant_id": "...", "domain": "cafe-torbat.example.com", "domain_type": "subdomain", "is_verified": true, "verified_at": null, "created_at": "..." } ], "primary_domain": "cafe-torbat.example.com" } ``` ## ۸. Identity & Access Service (فاز ۲) Base URL: `http://identity-access-service:8001` (Docker) یا `http://localhost:8001` | گروه | متد و مسیر | احراز هویت | | --- | --- | --- | | Health | `GET /health` | عمومی | | Auth Config | `GET /api/v1/auth/config` | عمومی | | Login URL | `GET /api/v1/auth/login-url` | عمومی | | Token Exchange | `POST /api/v1/auth/token` | عمومی (code) | | Current User | `GET /api/v1/auth/me` | JWT | | Register (SSO+mobile) | `POST /api/v1/auth/register` | عمومی | | Verify register mobile | `POST /api/v1/auth/register/verify-mobile` | عمومی | | Register mobile only | `POST /api/v1/auth/register/mobile` | عمومی | | Complete mobile register | `POST /api/v1/auth/register/mobile/verify` | عمومی | | **Unified mobile start** | `POST /api/v1/auth/mobile/start` | عمومی | | **Unified mobile complete** | `POST /api/v1/auth/mobile/complete` | عمومی | | **Session handoff redeem** | `POST /api/v1/auth/session/redeem` | عمومی (یکبارمصرف) | | SSO mobile verify request | `POST /api/v1/auth/mobile/request` | JWT | | SSO mobile verify | `POST /api/v1/auth/mobile/verify` | JWT | | Users | `POST /api/v1/users` | platform_admin | | Members | `POST/GET /api/v1/tenants/{id}/members` | platform_admin | ### جریان SSO (Frontend) 1. `GET /api/v1/auth/login-url` → redirect به Keycloak 2. Callback با `code` → `POST /api/v1/auth/token` 3. `GET /api/v1/auth/me` → اگر `requires_mobile_verification=true` → `/auth/verify-mobile` 4. استفاده از `access_token` در هدر `Authorization: Bearer ...` ### ورود مرکزی (Keycloak — تنها UX کاربر) 1. همه لینک‌های «ورود» → `GET /auth/login-url` → Keycloak 2. تب موبایل در Keycloak → `mobile/start` + `mobile/complete?create_handoff=true` 3. `GET /auth/callback?handoff=...` → `POST /session/redeem` → dashboard ### ورود/ثبت‌نام یکپارچه (API) 1. `POST /auth/mobile/start` با `{ mobile }` → `{ intent: "login"|"register", expires_in }` 2. کاربر OTP را وارد می‌کند 3. `POST /auth/mobile/complete` با `{ mobile, code, display_name?, email?, username?, password? }` - اگر `intent=login`: فقط mobile + code - اگر `intent=register`: فیلدهای پروفایل الزامی (یا ثبت‌نام ساده بدون آن‌ها برای موبایل-only) 4. پاسخ: `AuthSessionResponse` (access_token + refresh_token) → redirect به `/dashboard` ### ثبت‌نام legacy - **SSO + موبایل:** `POST /register` → OTP → `POST /register/verify-mobile` - **فقط موبایل:** `POST /register/mobile` → OTP → `POST /register/mobile/verify` - Identity برای OTP به Core متصل می‌شود: `POST core /api/v1/auth/otp/*` با `context=public` ### رویدادهای Identity | event_type | توضیح | | --- | --- | | user.registered | ثبت کاربر جدید | | tenant_member.added | افزودن عضو به tenant | | tenant_member.removed | حذف عضو از tenant | ## ۹. Service Registry سرویس‌های داخلی آینده باید خود را در `service_registry` ثبت کنند (`service_key`, `base_url`, `health_check_url`, `status`) تا discovery و health-check ممکن باشد.