Adds accounting-service PATCH/archive, fiscal helpers, COA templates and setup status, plus SuperApp Accounting UI (DS, scoreboard, masters, vouchers, ledger, ops modules) with session refresh and HTTPS public API URLs. Co-authored-by: Cursor <cursoragent@cursor.com>
276 lines
13 KiB
Markdown
276 lines
13 KiB
Markdown
# قراردادهای ارتباطی سرویسها (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 ممکن باشد.
|