# 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`) ```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_ids` | integer[] | ❌ | شهرهای تحت پوشش (چند-شهری). `city_id` تکی هم برای BC پذیرفته می‌شود | | `domain` | string | ❌ | دامنه اختصاصی نماینده (نرمال می‌شود: بدون scheme/www). یکتا؛ نباید با دامنه شهرها تداخل کند. **admin-only** | | `is_global` | boolean | ❌ | نماینده سراسری — سایتِ دامنه‌اش فقط پزشکان/کلینیک‌های خودش را نشان می‌دهد. **admin-only** | | `commission_percent` | float | ❌ | Commission rate (0–100) | | `bank_account` | object | ❌ | Bank details for settlements | **پاسخ‌ها اکنون شامل:** `city_ids: int[]`، `cities: [{id, name}]`، `domain`، `is_global` (علاوه بر `city_id` قدیمی = اولین شهر). ### Response `201` ```json { "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` ```json { "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`) ```json { "full_name": "علی احمدی جدید", "city_id": 50, "commission_percent": 12, "bank_account": { ... }, "active": true } ``` All fields optional. `city_ids: int[]` جایگزین `city_id` است (تکی هم پذیرفته می‌شود). **Privileged fields:** `commission_percent`، `active`، `domain` و `is_global` **admin-only** هستند — نماینده روی رکورد خودش فقط `full_name`، `city_ids`، `bank_account` را می‌تواند تغییر دهد. `commission_percent` باید در بازه `0–100` باشد. خطاهای `domain`: نامعتبر → 422، تکراری یا برخورد با دامنه شهر → 409. ### Response `200` Updated representation object. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_FORBIDDEN_001` | 403 | Not owner or admin | | `ERR_AUTH_006` | 403 | Non-admin tried to change `commission_percent` or `active` | | `ERR_VALIDATION_001` | 422 | `commission_percent` خارج از بازه ۰ تا ۱۰۰ (`field: commission_percent`) | | `ERR_NOT_FOUND_001` | 404 | Representation not found | --- ## DELETE `/api/v1/representation/{uuid}` Delete a representation. **Permission:** `ROLE_ADMIN` ### Response `200` ```json { "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 | ✅ | 1–12 | ### Response `200` ```json { "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` ```json { "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 } } } ``` --- ## GET `/api/v1/site-context` **عمومی (بدون auth).** نگاشت یک دامنه به زمینه‌ی سایت — مصرف‌کننده: سایت عمومی nobat724 برای دامنه‌های خارج از `data/city.json` (دامنه اختصاصی نمایندگان سراسری). ### Query Parameters | Param | Type | Required | Description | |-------|------|----------|-------------| | `domain` | string | ✅ | host یا URL کامل؛ نرمال می‌شود (scheme/www/پورت حذف) | ### Response `200` ```json { "success": true, "data": { "type": "representation", "city": null, "representation": { "uuid": "...", "full_name": "نماینده الف", "is_global": true } } } ``` `type`: `city` (دامنه یکی از شهرها) | `representation` (دامنه اختصاصی نماینده فعال) | `unknown`. برای `city`، آبجکت `city: {id, name}` پر می‌شود. --- ## قانون کمیسیون دامنه‌محور کمیسیون (نوبت **و** اشتراک) فقط وقتی ثبت می‌شود که **هر دو** شرط برقرار باشد: 1. دامنه‌ی مبدأ خرید (`payment.frontend_address`) متعلق به یک نماینده‌ی فعال باشد (`representations.domain`). 2. پزشک/کلینیکِ موضوع خرید، `representation_id` همان نماینده را داشته باشد. در غیر این صورت هیچ کمیسیونی برای هیچ نماینده‌ای ثبت نمی‌شود (پرداخت بدون `frontend_address` هم کمیسیون ندارد). درصد: نوبت = `commission_percent` نماینده؛ اشتراک = تنظیم سراسری `upgrade_commission_percent`. نگاشت دامنه فقط از طریق `DomainContextResolver` انجام می‌شود. --- ## پنل نماینده (ROLE_REPRESENTATION) این endpointها برای کاربرِ دارای نقش `ROLE_REPRESENTATION` در پنل ادمین (`/admin`) هستند. مالکیت همیشه از کاربر جاری (`#[CurrentUser]` + `findByUser`) تعیین می‌شود؛ هیچ uuid/id ورودی برای تعیین مالکیت پذیرفته نمی‌شود. > **Permission (همه‌ی این بخش):** `ROLE_REPRESENTATION` ### GET `/api/v1/representation/me` پروفایل نماینده‌ی کاربر جاری. #### Response `200` ```json { "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 ```json { "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 ```json { "iban": "IR000000000000000000000000", "birth_date": "1370/01/01" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `iban` | string | ✅ | شماره شبا (با/بدون `IR` و فاصله؛ نرمال‌سازی می‌شود) | | `birth_date` | string | ✅ | تاریخ تولد شمسی `Y/m/d` (نمونه `1370/01/01`). فقط برای استعلام IbanMatch؛ **ذخیره نمی‌شود.** | #### Response `200` آبجکت پروفایل نماینده با `bank_account` به‌روزشده. #### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_IDENTITY_004` | 409 | کد ملی هنوز تأیید نشده | | `ERR_IDENTITY_003` | 409 | سقف ۲ شبا پر است | | `ERR_VALIDATION_001` | 422 | شبا نامعتبر (`field: iban`) یا تاریخ تولد نامعتبر (`field: birth_date`) | | `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 ```json { "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 | ❌ | | `activity_time` | integer (Unix ts، تاریخ شروع فعالیت) | ❌ | | `specialties` | integer[] | ❌ | #### Response `201` ```json { "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 ```json { "owner_mobile": "0935...", "name": "کلینیک ...", "telephone": "...", "address": "..." } ``` | Field | Type | Required | |-------|------|----------| | `owner_mobile` | string | ✅ | | `name` | string | ✅ | | `telephone` / `address` / `info` | string | ❌ | #### Response `200` ```json { "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` ```json { "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` ```json { "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` ```json { "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` ```json { "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` ```json { "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`:** ```json { "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` = موجودی کیف‌پول (`getWalletBalance`)؛ `settled_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):** ```json { "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):** ```json { "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) همان نماینده محاسبه می‌شود.