TorbatYar/docs/services_contracts.md
Mortezakoohjani 800b0ba2c5 Deploy TorbatYar for torbatyar.ir with nginx multi-tenant routing.
Wire production domain, CORS for tenant subdomains, celery volume mounts, and nginx reverse proxy configs for apex, API, identity, auth, and wildcard tenants.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 21:43:33 +03:30

271 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# قراردادهای ارتباطی سرویس‌ها (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 <token>` — چه 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 ممکن باشد.