Add independent payment-service (port 8012, payment_db) with foundation licensing, BYO-PSP, merchant accounts, idempotent requests, callbacks, and immutable ledger. Co-authored-by: Cursor <cursoragent@cursor.com>
16 KiB
قراردادهای ارتباطی سرویسها (Service Contracts)
Reference only. Event rules → event-driven-architecture.md.
API index → api-reference.md · Event list → event-catalog.md.
۱. قوانین بنیادی
- ارتباط مستقیم بین دیتابیس سرویسها ممنوع است. هیچ سرویسی نباید به دیتابیس سرویس دیگر کوئری بزند.
- هر سرویس فقط دیتابیس خودش را میشناسد (Database-per-service).
- ارتباط بین سرویسها فقط از این چهار راه:
- REST API (همزمان)
- Webhook (اعلان به بیرون)
- Async Event (message bus)
- Outbox/Inbox Pattern (تحویل قابل اعتماد رویداد)
- همه درخواستهای tenant-aware باید
tenant_idرا حمل کنند (هدرX-Tenant-IDیا داخل توکن/رویداد).
۲. احراز هویت بین سرویسها
- سرویسها با Internal Service Token یکدیگر را احراز میکنند.
- توکن بهصورت hash در
internal_service_tokensذخیره میشود (خود توکن ذخیره نمیشود). - هر توکن دارای
scopesاست؛ سرویس فراخوان باید scope لازم را داشته باشد. - کاربر نهایی با JWT کیکلوک احراز میشود.
۳. قرارداد رویداد (Event Envelope)
همه رویدادها با ساختار مشترک shared.events.EventEnvelope منتشر میشوند:
{
"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
- سرویس مبدأ رویداد را در
outbox_events(در همان تراکنش) ذخیره میکند. - worker رویدادهای
pendingرا منتشر وprocessedمیکند. - سرویس مقصد رویداد را در
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برگردانده میشوند:
{ "success": false, "error": { "code": "not_found", "message": "...", "details": null } }
- لیستها با قالب صفحهبندی
Page(شاملitemsوmeta).
۷. Onboarding و Tenant Context (فاز ۴ — Tenant Onboarding و Workspace Activation)
در بریف پروژه این مجموعه API با عنوان «Phase 3» شناخته میشود؛ در شمارهگذاری داخلی مستندات فاز ۴ است (نگاه کنید به progress.md و phase-numbering).
همهٔ endpointهای این بخش زیر API_V1_PREFIX (پیشفرض /api/v1) هستند و به
احراز هویت نیاز دارند (Authorization: Bearer <token> — چه JWT محلی OTP و
چه JWT کیکلوک SSO؛ هر دو با get_current_core_user resolve میشوند).
GET /api/v1/me
وضعیت کاربر جاری، عضویتها و نیاز به onboarding.
پاسخ ۲۰۰:
{
"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).
GET /api/v1/me/tenants
آرایهای از MembershipSummary (همان ساختار memberships بالا) برای همهٔ
tenantهای در دسترس کاربر جاری.
POST /api/v1/onboarding/tenant
ساخت tenant جدید توسط کاربر جاری (بدون نیاز به عضویت قبلی).
بدنه درخواست:
{
"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
{ "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
{ "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
{ "tenant_id": "uuid" }
current_tenant_id کاربر را به این tenant تغییر میدهد (فقط اگر عضویت
active روی آن داشته باشد). پاسخ ۲۰۰: TenantContextRead.
خطا: 403 forbidden (عضو نیست یا عضویت غیرفعال).
ساختار مشترک TenantContextRead
{
"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)
GET /api/v1/auth/login-url→ redirect به Keycloak- Callback با
code→POST /api/v1/auth/token GET /api/v1/auth/me→ اگرrequires_mobile_verification=true→/auth/verify-mobile- استفاده از
access_tokenدر هدرAuthorization: Bearer ...
ورود مرکزی (Keycloak — تنها UX کاربر)
- همه لینکهای «ورود» →
GET /auth/login-url→ Keycloak - تب موبایل در Keycloak →
mobile/start+mobile/complete?create_handoff=true GET /auth/callback?handoff=...→POST /session/redeem→ dashboard
ورود/ثبتنام یکپارچه (API)
POST /auth/mobile/startبا{ mobile }→{ intent: "login"|"register", expires_in }- کاربر OTP را وارد میکند
POST /auth/mobile/completeبا{ mobile, code, display_name?, email?, username?, password? }- اگر
intent=login: فقط mobile + code - اگر
intent=register: فیلدهای پروفایل الزامی (یا ثبتنام ساده بدون آنها برای موبایل-only)
- اگر
- پاسخ:
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 ممکن باشد.
۱۰. Payment Platform (Torbat Pay) — planned
قراردادهای کامل: payment-contracts.md · ADR: ADR-020, ADR-021
Entitlement (Core L1)
| feature_key | Meaning |
|---|---|
payment.module.enabled |
Tenant may use Torbat Pay |
Integration rules
- Verticals never store PSP credentials — only
payment_request_ref,payment_transaction_ref, orcheckout_session_ref. - Paid state: subscribe to
payment.request.paidor pollGET /api/v1/payment-transactions/{id}with idempotency. - Settlement: consume
payment.accounting_intent.createdin Accounting — Payment does not post journals. - Internal calls: Internal Service Token +
X-Tenant-ID+ scope e.g.payment.requests.create.
Contract versions (stable before implementation)
payment_intent.v1, checkout_session.v1, callback_ingress.v1, settlement_intent.v1, plus reserved: refund_intent.v1, split_allocation.v1, subscription_billing.v1, wallet_topup.v1, installment_plan.v1.
۱۱. Published Resource (publish_id) — architecture contracts
قراردادهای کامل: published-resource-contracts.md · ADR: ADR-022 · Architecture: published-resource-architecture.md
قوانین
- هر سطح عمومی قابلاتصال برای سرویسهای دیگر باید
publish_idداشته باشد. - Payment، Communication، Short Link، QR، Analytics، CRM و verticalها فقط با
publish_idوصل میشوند — نه جداول داخلی Experience. - Form / Survey / Appointment باید standalone publish شوند (وابستگی اجباری به Site/Page ممنوع).
- Published Actions، Public Access، و Universal Embed قراردادهای additive هستند — رجیستریها بازند؛ جزئیات در published-action-registry.md، public-access-contract.md، embed-contract.md.
- Experience مالک authentication / payment / membership نیست.
- پیادهسازی Registry/API تا ثبت فاز آینده ممنوع است (این بخش فقط قرارداد است).