Files
clinicpro/docs/api/representation.md
T
hamed c2c6ae4d02 feat(migrations): add national_code_verified flag to users and normalize bank_account representation
- Added a new column `national_code_verified` to the `users` table.
- Normalized the `bank_account` field in the `representations` table from a single object to an array of IBANs with a default `verified` status of false.

feat(ApiIrService): implement identity verification client for api.ir

- Created `ApiIrService` to handle identity verification via api.ir.
- Implemented methods for matching national code with mobile and IBAN with national code and birth date.
- Added error handling and logging for external API requests.
2026-06-25 19:38:47 +03:30

20 KiB
Raw Blame History

Representation (Agent) API

Prefix: /api/v1/representation

Representations are sales agents who earn commission on appointments booked through their referral.


POST /api/v1/representation

Create a new representation.

Permission: ROLE_ADMIN

Request Body (application/json)

{
  "full_name": "علی احمدی",
  "mobile_number": "09123456789",
  "city_id": 42,
  "commission_percent": 10,
  "bank_account": {
    "iban": "IR...",
    "account_number": "1234567890",
    "bank_name": "بانک ملت",
    "owner_name": "علی احمدی"
  }
}
Field Type Required Description
full_name string Agent full name
mobile_number string Login mobile (creates a User account)
city_id integer City ID (FK to categories where bundle=city)
commission_percent float Commission rate (0100)
bank_account object Bank details for settlements

Response 201

{
  "success": true,
  "data": {
    "uuid": "rep-uuid-...",
    "full_name": "علی احمدی",
    "mobile_number": "09123456789",
    "city_id": 42,
    "commission_percent": 10,
    "active": true,
    "bank_account": { ... },
    "created_at": 1717000000
  }
}

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_AUTH_006 403 Not admin
ERR_CONFLICT_001 409 Mobile number already in use
ERR_VALIDATION_001 422 mobile_number فرمت معتبر موبایل ایران (^09\d{9}$) ندارد (field: mobile_number)
ERR_VALIDATION_002 422 mobile_number یا full_name خالی

GET /api/v1/representation/{uuid}

Get representation detail.

Permission: AUTH — must be the representation's user or ROLE_ADMIN

Path Parameters

Param Type Description
uuid string (UUID) Representation UUID

Response 200

{
  "success": true,
  "data": {
    "uuid": "...",
    "full_name": "علی احمدی",
    "mobile_number": "09123456789",
    "city_id": 42,
    "city_name": "تهران",
    "commission_percent": 10,
    "active": true,
    "bank_account": { ... },
    "created_at": 1717000000
  }
}

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_FORBIDDEN_001 403 Not owner or admin
ERR_NOT_FOUND_001 404 Representation not found

PATCH /api/v1/representation/{uuid}

Update representation.

Permission: AUTH — must be the representation's user or ROLE_ADMIN

Request Body (application/json)

{
  "full_name": "علی احمدی جدید",
  "city_id": 50,
  "commission_percent": 12,
  "bank_account": { ... },
  "active": true
}

All fields optional.

Response 200

Updated representation object.

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_FORBIDDEN_001 403 Not owner or admin
ERR_NOT_FOUND_001 404 Representation not found

DELETE /api/v1/representation/{uuid}

Delete a representation.

Permission: ROLE_ADMIN

Response 200

{ "success": true, "data": { "message": "نماینده حذف شد" } }

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_AUTH_006 403 Not admin
ERR_NOT_FOUND_001 404 Representation not found

GET /api/v1/representation/{uuid}/dashboard/monthly

Get monthly earnings dashboard for a representation.

Permission: AUTH — must be the representation's user or ROLE_ADMIN

Path Parameters

Param Type Description
uuid string (UUID) Representation UUID

Query Parameters

Param Type Required Description
year integer e.g. 2024
month integer 112

Response 200

{
  "success": true,
  "data": {
    "period": { "year": 2024, "month": 6 },
    "stats": {
      "total_appointments": 15,
      "total_revenue_rials": 7500000,
      "commission_rials": 750000,
      "daily": [
        { "date": "2024-06-01", "appointments": 2, "revenue": 1000000, "commission": 100000 }
      ]
    }
  }
}

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_FORBIDDEN_001 403 Not owner or admin
ERR_NOT_FOUND_001 404 Representation not found

GET /api/v1/representation/{uuid}/dashboard/yearly

Get yearly earnings dashboard for a representation.

Permission: AUTH — must be the representation's user or ROLE_ADMIN

Query Parameters

Param Type Required Description
year integer e.g. 2024

Response 200

{
  "success": true,
  "data": {
    "period": { "year": 2024 },
    "months": [
      { "month": 1, "appointments": 10, "revenue_rials": 5000000, "commission_rials": 500000 },
      { "month": 2, "appointments": 8, "revenue_rials": 4000000, "commission_rials": 400000 }
    ],
    "totals": {
      "appointments": 97,
      "revenue_rials": 48500000,
      "commission_rials": 4850000
    }
  }
}

پنل نماینده (ROLE_REPRESENTATION)

این endpointها برای کاربرِ دارای نقش ROLE_REPRESENTATION در پنل ادمین (/admin) هستند. مالکیت همیشه از کاربر جاری (#[CurrentUser] + findByUser) تعیین می‌شود؛ هیچ uuid/id ورودی برای تعیین مالکیت پذیرفته نمی‌شود.

Permission (همه‌ی این بخش): ROLE_REPRESENTATION

GET /api/v1/representation/me

پروفایل نماینده‌ی کاربر جاری.

Response 200

{
  "success": true,
  "data": {
    "data": {
      "uuid": "...",
      "full_name": "حامد حسینی",
      "mobile_number": "09120671756",
      "city_id": 132,
      "commission_percent": "10.00",
      "bank_account": [
        { "id": "iban-uuid-1", "iban": "IR000000000000000000000000", "bank_name": "بانک ملت", "owner_name": "حامد حسینی", "verified": true, "created_at": 1718000000 }
      ],
      "active": true,
      "created_at": 1718000000,
      "national_code": "0012345678",
      "national_code_verified": true
    }
  }
}

double-nested: مقدار با data.data استخراج می‌شود. bank_account آرایه‌ای از ۰ تا ۲ شبا است (null اگر هیچ شبایی ثبت نشده). national_code/national_code_verified از کاربرِ نماینده می‌آید.

Errors

Code HTTP Description
ERR_NOT_FOUND_001 404 کاربر جاری نماینده نیست

POST /api/v1/representation/verify-national-code

تأیید کد ملی نماینده با استعلام شاهکار (s.api.ir → ShahkarLite): تطبیق کد ملی با موبایلِ کاربر جاری. در صورت موفقیت، national_code ذخیره و national_code_verified=true می‌شود.

Request Body

{ "national_code": "0012345678" }
Field Type Required Description
national_code string کد ملی ۱۰ رقمی

Response 200

آبجکت پروفایل نماینده (مثل me، با national_code_verified: true).

Errors

Code HTTP Description
ERR_VALIDATION_001 422 کد ملی ۱۰ رقم نیست (field: national_code)
ERR_IDENTITY_001 422 کد ملی متعلق به این موبایل نیست (field: national_code)
ERR_EXTERNAL_001 502 خطا در استعلام
ERR_EXTERNAL_002 503 سرویس استعلام پیکربندی نشده

POST /api/v1/representation/iban

افزودن یک شماره شبا. ابتدا با IbanMatch (s.api.ir) بررسی می‌شود شبا متعلق به کد ملیِ تأییدشده‌ی نماینده باشد. حداکثر ۲ شبا.

Request Body

{ "iban": "IR000000000000000000000000" }
Field Type Required Description
iban string شماره شبا (با/بدون IR و فاصله؛ نرمال‌سازی می‌شود)

Response 200

آبجکت پروفایل نماینده با bank_account به‌روزشده.

Errors

Code HTTP Description
ERR_IDENTITY_004 409 کد ملی هنوز تأیید نشده
ERR_IDENTITY_003 409 سقف ۲ شبا پر است
ERR_VALIDATION_001 422 شبا نامعتبر (field: iban)
ERR_IDENTITY_002 422 شبا متعلق به نماینده نیست (field: iban)
ERR_EXTERNAL_001 502 خطا در استعلام
ERR_EXTERNAL_002 503 سرویس استعلام پیکربندی نشده

DELETE /api/v1/representation/iban/{id}

حذف یک شماره شبا با id آن (از bank_account[].id).

Response 200

آبجکت پروفایل نماینده با bank_account به‌روزشده.


POST /api/v1/representation/doctor

افزودن پزشک توسط نماینده. representation_id پزشک به‌صورت خودکار روی نماینده‌ی کاربر جاری ست می‌شود. پس از ثبت موفق، یک پیامک خوش‌آمد (تگ welcome) به‌صورت async به موبایل پزشک ارسال می‌شود.

Request Body

{ "mobile": "0935...", "name": "دکتر ...", "gender": "man", "degree": "...", "medical_system_code": "...", "specialties": [1,2] }
Field Type Required
mobile string
name string
gender / degree / medical_system_code / info string
specialties integer[]

Response 201

{ "success": true, "data": { "uuid": "..." } }

Errors

Code HTTP Description
ERR_VALIDATION_002 422 موبایل یا نام خالی
ERR_VALIDATION_001 422 mobile فرمت معتبر موبایل ایران ندارد (field: mobile)
ERR_CONFLICT_001 409 این کاربر قبلاً پزشک است

POST /api/v1/representation/clinic

افزودن کلینیک توسط نماینده. representation_id کلینیک خودکار روی نماینده‌ی کاربر جاری ست می‌شود (مثل createDoctor) تا در لیست‌های scoped دیده شود. پس از ثبت موفق، یک پیامک خوش‌آمد (تگ welcome) به‌صورت async به موبایل مالک کلینیک ارسال می‌شود.

Request Body

{ "owner_mobile": "0935...", "name": "کلینیک ...", "telephone": "...", "address": "..." }
Field Type Required
owner_mobile string
name string
telephone / address / info string

Response 200

{ "success": true, "data": { "uuid": "...", "name": "...", "is_active": true } }

Errors

Code HTTP Description
ERR_VALIDATION_002 422 موبایل یا نام خالی
ERR_VALIDATION_001 422 owner_mobile فرمت معتبر موبایل ایران ندارد (field: owner_mobile)

GET /api/v1/representation/appointments

نوبت‌های همه‌ی پزشکانی که representation_id آن‌ها = نماینده‌ی کاربر جاری است (paginated، با شکل آیتمِ یکسان با /api/v1/admin/appointments).

Query Parameters

Param Type Required Description
page integer پیش‌فرض 1
limit integer پیش‌فرض 15، حداکثر 500
status string فیلتر وضعیت
date string (YYYY-MM-DD) فیلتر تاریخِ نوبت
search string جستجو در موبایل/نام بیمار یا نام پزشک

Response 200

{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "patient_name": "...",
      "patient_mobile": "0912...",
      "doctor_uuid": "...",
      "doctor_name": "دکتر ...",
      "slot_start": 1718000000,
      "slot_end": 1718001800,
      "appointment_date": "2025-06-15",
      "appointment_time": "10:00",
      "end_time": "10:30",
      "status": "confirmed",
      "created_at": 1717900000
    }
  ],
  "meta": { "totalRecords": 12, "totalPages": 1, "currentPage": 1 }
}

Errors

Code HTTP Description
ERR_NOT_FOUND_001 404 کاربر جاری نماینده نیست

GET /api/v1/representation/doctors

پزشکانِ ثبت‌شده توسط نماینده‌ی جاری (فقط ردیف‌های representation_id = نماینده‌ی کاربر جاری). شکل آیتم یکسان با GET /api/v1/admin/doctors است.

Permission: ROLE_REPRESENTATION — id نماینده از #[CurrentUser] تعیین می‌شود، نه از query (نماینده نمی‌تواند داده‌ی نماینده‌ی دیگر را ببیند).

Query Parameters

Param Type Required Description
page integer پیش‌فرض 1
limit integer پیش‌فرض 15، حداکثر 100
search string جستجو در نام یا موبایل پزشک

Response 200

{
  "success": true,
  "data": [
    {
      "uuid": "...", "id": 12, "name": "دکتر ...", "gender": "man", "degree": "...",
      "medical_code": "...", "mobile": "0912...", "email": null,
      "is_active": true, "rate": 3.5, "specialties": [],
      "profile_image": null, "created_at": "2026-06-18T..."
    }
  ],
  "meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 }
}

Errors

Code HTTP Description
ERR_NOT_FOUND_001 404 کاربر جاری نماینده نیست

GET /api/v1/representation/doctors/stats

آمار پزشکانِ ثبت‌شده توسط نماینده‌ی جاری (فقط representation_id = نماینده‌ی کاربر جاری). شکل پاسخ سازگار با GET /api/v1/admin/doctors/stats (بدون top_specialty). فرانت‌اند کارت‌های «کل پزشکان / فعال / غیرفعال / مرد / زن» را از این endpoint برای نقش نماینده پر می‌کند.

Permission: ROLE_REPRESENTATION — id نماینده از #[CurrentUser].

Response 200

{
  "success": true,
  "data": { "total": 12, "active": 9, "inactive": 3, "male": 7, "female": 5 }
}

Errors

Code HTTP Description
ERR_NOT_FOUND_001 404 کاربر جاری نماینده نیست

POST /api/v1/representation/doctors/{uuid}/status

فعال/غیرفعال کردن پزشکِ زیرمجموعه‌ی نماینده‌ی جاری (toggle active_doctor_appointment). فقط روی پزشکانی که representation_id آن‌ها برابر نماینده‌ی کاربر جاری است؛ در غیر این صورت 404.

Permission: ROLE_REPRESENTATION — مالکیت از #[CurrentUser] چک می‌شود.

Path Parameters

Param Type Description
uuid string uuid پزشک

Response 200

{ "success": true, "data": { "is_active": false } }

Errors

Code HTTP Description
ERR_NOT_FOUND_001 404 کاربر جاری نماینده نیست، یا پزشک یافت نشد / متعلق به این نماینده نیست

GET /api/v1/representation/clinics

کلینیک‌های ثبت‌شده توسط نماینده‌ی جاری (فقط representation_id = نماینده‌ی کاربر جاری). شکل آیتم سازگار با GET /api/v1/admin/clinics.

Permission: ROLE_REPRESENTATION — id نماینده از #[CurrentUser].

Query Parameters

Param Type Required Description
page integer پیش‌فرض 1
limit integer پیش‌فرض 15، حداکثر 100
search string جستجو در نام یا تلفن کلینیک

Response 200

{
  "success": true,
  "data": [
    {
      "uuid": "...", "id": 5, "name": "کلینیک ...", "telephone": "...",
      "logo": null, "clinic_logo": null, "is_active": true,
      "doctors_count": 0, "created_at": "2026-06-18T..."
    }
  ],
  "meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 }
}

Errors

Code HTTP Description
ERR_NOT_FOUND_001 404 کاربر جاری نماینده نیست

داشبورد، عملکرد و مالیِ نماینده‌ی جاری

همه‌ی این endpointها #[IsGranted('ROLE_REPRESENTATION')] و scope بر اساس #[CurrentUser] (نه uuid مسیر). درآمد همیشه از FinancialBreakdown.representation_share_rials (پورسانت واقعیِ ثبت‌شده) محاسبه می‌شود، نه مبلغ کل نوبت. بازه‌ها: امروز=strtotime('today'), هفته=۷ روز اخیر, ماه=۳۰ روز اخیر.

GET /api/v1/representation/dashboard/summary

خلاصه‌ی آمار نوبت و درآمد نماینده‌ی جاری.

Response 200:

{
  "success": true,
  "data": {
    "appointments": { "today": 0, "week": 3, "month": 12, "total": 40 },
    "income": {
      "today": 0, "week": 270000, "month": 909090, "total": 3000000,
      "settlable_rials": 2090910, "settled_rials": 500000, "pending_rials": 0
    }
  }
}

settlable_rials = موجودی کیف‌پول (getWalletBalancesettled_rials = جمع Settlementهای paid؛ pending_rials = جمع pending+approved.

Errors

Code HTTP Description
ERR_NOT_FOUND_001 404 کاربر جاری نماینده نیست

GET /api/v1/representation/doctors/performance

عملکرد پزشکانِ نماینده‌ی جاری (paginated). Query: page, limit.

Response 200 (paginated):

{
  "success": true,
  "data": [
    {
      "uuid": "...", "name": "دکتر ...",
      "appointments": { "today": 0, "week": 1, "month": 4, "total": 18 },
      "representation_income_rials": 363636,
      "subscription_status": "active"
    }
  ],
  "meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 }
}

subscription_status: active (اشتراک فعال دارد) یا none.

GET /api/v1/representation/finance/report

گزارش مالی بازه‌ای از ردیف‌های FinancialBreakdown نماینده‌ی جاری (paginated). Query: page, limit, from (Unix ts), to (Unix ts).

Response 200 (paginated):

{
  "success": true,
  "data": [
    {
      "uuid": "...", "appointment_uuid": "...", "doctor_name": "دکتر ...",
      "gross_rials": 2000000, "tax_rials": 45455, "sms_fee_rials": 1500000,
      "commission_percent": 20, "representation_share_rials": 90909,
      "created_at": "2026-06-24T..."
    }
  ],
  "meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 }
}

اصلاح buildStats (در GET /api/v1/representation/{uuid}/dashboard/monthly|yearly): قبلاً آمار را به نماینده فیلتر نمی‌کرد (کلِ پلتفرم). اکنون total_appointments فقط نوبت‌های پزشکانِ همان نماینده، commission_rials از FinancialBreakdown.representation_share_rials، و total_revenue_rials از FinancialBreakdown.gross_rials (source=appointment) همان نماینده محاسبه می‌شود.