# قراردادهای ارتباطی سرویس‌ها (Service Contracts)

> Reference only. Event rules → [event-driven-architecture.md](../architecture/event-driven-architecture.md).  
> API index → [api-reference.md](api-reference.md) · Event list → [event-catalog.md](event-catalog.md).

## ۱. قوانین بنیادی
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](../progress.md)
> و [phase-numbering](../decisions/technical/phase-numbering.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` در حالات resolve نامعتبر
(JIT معمولاً کاربر Core را لینک/می‌سازد — نگاه کنید به
[identity-architecture.md](../architecture/identity-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 ممکن باشد.
