TorbatYar/docs/reference/services-contracts.md
Mortezakoohjani 9fac160258 feat(payment): ship Torbat Pay MVP phases 14.0-14.5
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>
2026-07-27 17:57:04 +03:30

16 KiB
Raw Permalink Blame History

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

Reference only. Event rules → event-driven-architecture.md.
API index → api-reference.md · Event list → 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 منتشر می‌شوند:

{
  "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 برگردانده می‌شوند:
{ "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)

  1. GET /api/v1/auth/login-url → redirect به Keycloak
  2. Callback با codePOST /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 ممکن باشد.

۱۰. 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

  1. Verticals never store PSP credentials — only payment_request_ref, payment_transaction_ref, or checkout_session_ref.
  2. Paid state: subscribe to payment.request.paid or poll GET /api/v1/payment-transactions/{id} with idempotency.
  3. Settlement: consume payment.accounting_intent.created in Accounting — Payment does not post journals.
  4. 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

قوانین

  1. هر سطح عمومی قابل‌اتصال برای سرویس‌های دیگر باید publish_id داشته باشد.
  2. Payment، Communication، Short Link، QR، Analytics، CRM و verticalها فقط با publish_id وصل می‌شوند — نه جداول داخلی Experience.
  3. Form / Survey / Appointment باید standalone publish شوند (وابستگی اجباری به Site/Page ممنوع).
  4. Published Actions، Public Access، و Universal Embed قراردادهای additive هستند — رجیستری‌ها بازند؛ جزئیات در published-action-registry.md، public-access-contract.md، embed-contract.md.
  5. Experience مالک authentication / payment / membership نیست.
  6. پیاده‌سازی Registry/API تا ثبت فاز آینده ممنوع است (این بخش فقط قرارداد است).