feat: add multi-city representation support and domain context resolution

- Created migration to add representation_cities table and domain, is_global fields to representations.
- Implemented SiteContextController to resolve domain to site context (city | representation | unknown).
- Developed DomainContext and DomainContextResolver services for domain mapping.
- Added tests for DomainContextResolver and commission logic based on domain ownership.
This commit is contained in:
hamed
2026-07-09 07:26:58 +03:30
parent 59559e2e31
commit 0750bc9812
56 changed files with 4297 additions and 1600 deletions
+41 -3
View File
@@ -32,10 +32,14 @@ Create a new representation.
|-------|------|----------|-------------|
| `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) |
| `city_ids` | integer[] | ❌ | شهرهای تحت پوشش (چند-شهری). `city_id` تکی هم برای BC پذیرفته می‌شود |
| `domain` | string | ❌ | دامنه اختصاصی نماینده (نرمال می‌شود: بدون scheme/www). یکتا؛ نباید با دامنه شهرها تداخل کند. **admin-only** |
| `is_global` | boolean | ❌ | نماینده سراسری — سایتِ دامنه‌اش فقط پزشکان/کلینیک‌های خودش را نشان می‌دهد. **admin-only** |
| `commission_percent` | float | ❌ | Commission rate (0100) |
| `bank_account` | object | ❌ | Bank details for settlements |
**پاسخ‌ها اکنون شامل:** `city_ids: int[]`، `cities: [{id, name}]`، `domain`، `is_global` (علاوه بر `city_id` قدیمی = اولین شهر).
### Response `201`
```json
{
@@ -119,9 +123,9 @@ Update representation.
}
```
All fields optional.
All fields optional. `city_ids: int[]` جایگزین `city_id` است (تکی هم پذیرفته می‌شود).
**Privileged fields:** `commission_percent` and `active` are **admin-only** — a representation editing its own record may change `full_name`, `city_id`, `bank_account` but **not** these two. `commission_percent` must be within `0100`.
**Privileged fields:** `commission_percent`، `active`، `domain` و `is_global` **admin-only** هستند — نماینده روی رکورد خودش فقط `full_name`، `city_ids`، `bank_account` را می‌تواند تغییر دهد. `commission_percent` باید در بازه `0100` باشد. خطاهای `domain`: نامعتبر → 422، تکراری یا برخورد با دامنه شهر → 409.
### Response `200`
Updated representation object.
@@ -233,6 +237,40 @@ Get yearly earnings dashboard for a representation.
---
## 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 ورودی برای تعیین مالکیت پذیرفته نمی‌شود.