Files
clinicpro/docs/api/README.md
T
hamedandClaude Opus 4.8 3363dfbf22 feat(doctor): expose city/state in public doctor list
The public doctor list had no location field, so multi-domain consumers
could not tell which city domain owns a doctor. nobat724_front's sitemap
worked around this by fetching the list once per city (35 sweeps) and
subtracting, costing ~13s to build the root sitemap.

Location is resolved in bulk by DoctorRepository::findLocationsByDoctors
using the same rule the city_id/state_id filter applies: the doctor's own
address first, falling back to the address of a clinic they belong to.
Without the clinic fallback a doctor could match city_id=X yet report no
city, which would break the sitemap's per-domain partitioning.

city/state are arrays with at most one entry, matching the shape already
used by the doctor detail response and the clinic list. A doctor with no
address reports [] rather than null. Multi-location doctors get a single
primary city, mirroring the canonical rule on the public site.

Also surface the applied page size as meta.limit. Repositories silently
clamp limit to 50, which previously made clients believe pagination had
ended early — this is what truncated the sitemap to 50 doctors.

The clinic doctor-list endpoint gets the same location data so both
endpoints agree.

Location resolution costs at most 2 queries regardless of page size,
asserted directly against the repository rather than through the
endpoint, since the endpoint carries a pre-existing specialties N+1 in
findWithFilters that is unrelated to this change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 08:08:48 +03:30

126 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ClinicPro — API Documentation Index
> **Base URL:** `https://clinic-pro.ddev.site`
> **API Prefix:** `/api/v1`
> **Swagger UI:** `https://clinic-pro.ddev.site/api/doc` — user: `admin` / pass: `clinic123`
---
## Authentication
All protected endpoints require:
```
Authorization: Bearer <JWT_TOKEN>
```
| Role | Description |
|------|-------------|
| `PUBLIC` | No token required |
| `AUTH` | Any valid JWT |
| `ROLE_ADMIN` | Admin user |
| `ROLE_DOCTOR` | Doctor user |
| `ROLE_CLINIC` | Clinic owner |
| `ROLE_SECRETARY` | Secretary |
---
## Standard Response Envelope
```json
// Success
{ "success": true, "data": { ... } }
// Paginated
{ "success": true, "data": [...], "meta": { "totalRecords": 100, "totalPages": 5, "currentPage": 1, "limit": 20 } }
// Error
{ "success": false, "data": null, "errors": [{ "code": "ERR_XXX_000", "message": "..." }] }
```
> `meta.limit` اندازهٔ صفحهٔ **واقعاً اعمال‌شده** است. ریپازیتوری‌ها `limit` درخواستی را به سقف خودشان کاهش می‌دهند (مثلاً لیست پزشکان: سقف ۵۰)، پس برای پیمایش کامل به `meta.totalPages` تکیه کن — نه به این فرض که «تعداد آیتم کمتر از limit درخواستی یعنی صفحهٔ آخر».
---
## Persian digit normalization (global)
Persian (`۰-۹`) and Arabic (`٠-٩`) digits sent in numeric request fields are translated to Latin **server-side, before the controller runs**`src/Shared/EventSubscriber/NumericFieldNormalizerSubscriber.php`. Every client benefits: the React admin panel, `nobat724_front`, and `clinic-pro-tauri`.
Applies to `POST` / `PUT` / `PATCH` requests under `/api/v1/` with a JSON body, recursively through nested arrays.
**Normalized keys:**
```
mobile, mobile_number, telephone, phone, notification_mobile,
national_code, postal_code,
card_number, account_number, sheba, shaba, iban,
price_rials, amount_rials, amount, free_visit_price_rials,
insurance_price_rials, patient_share_rials, visit_price_rials,
duration_minutes, duration, commission_percent, coverage,
coverage_percent, franchise, ceiling, tax_percent,
base_insurance_discount_percent, supplementary_discount_percent
```
Only **digits** are translated — no characters are stripped, so `IR` in a sheba and `-` in a landline survive. Non-string values (`int`, `bool`, `null`) and keys outside the list are untouched, so a name like `منشی شماره ۲` keeps its Persian digit.
```jsonc
// request
{ "mobile_number": "۰۹۱۲۳۴۵۶۷۸۹", "national_code": "۰۰۱۲۳۴۵۶۷۸", "name": "منشی شماره ۲" }
// what the controller sees
{ "mobile_number": "09123456789", "national_code": "0012345678", "name": "منشی شماره ۲" }
```
> Adding a new numeric field to any endpoint? Add its key to `NUMERIC_KEYS` in the subscriber, otherwise Persian digits reach the database.
---
## Modules
| File | Domain | Endpoints |
|------|--------|-----------|
| [auth.md](auth.md) | Authentication — OTP, Login, JWT | 8 |
| [doctor.md](doctor.md) | Doctor profile & addresses | 11 |
| [clinic.md](clinic.md) | Clinics | 7 |
| [clinic-invitation.md](clinic-invitation.md) | Doctor invitations to clinics | 8 |
| [appointment.md](appointment.md) | Appointments & slot booking | 6 |
| [appointment-settings.md](appointment-settings.md) | Weekly schedule, date overrides, holidays | 14 |
| [payment.md](payment.md) | Payments (Mellat / Sep) | 5 |
| [settlement.md](settlement.md) | Wallet & settlement requests | 7 |
| [rating.md](rating.md) | Ratings, comments, likes | 9 |
| [secretary.md](secretary.md) | Doctor secretaries | 5 |
| [representation.md](representation.md) | Representations (agents) | 6 |
| [sms.md](sms.md) | SMS send & templates | 10 |
| [blog.md](blog.md) | Blog posts | 6 |
| [specialty.md](specialty.md) | Medical specialties | 5 |
| [insurance.md](insurance.md) | Insurances & doctor-insurance links | 10 |
| [doctor-service.md](doctor-service.md) | Doctor services | 5 |
| [tag.md](tag.md) | Blog tags | 5 |
| [location.md](location.md) | Provinces & cities | 10 |
| [user-profile.md](user-profile.md) | User medical profile | 4 |
| [admin.md](admin.md) | Admin dashboard & management | 25+ |
---
## Error Code Reference
| Code | Message (FA) | HTTP |
|------|--------------|------|
| `ERR_AUTH_001` | توکن JWT منقضی یا نامعتبر | 401 |
| `ERR_AUTH_002` | کد OTP نامعتبر | 401 |
| `ERR_AUTH_003` | کد OTP منقضی شده | 401 |
| `ERR_AUTH_004` | تعداد تلاش‌های OTP به حد مجاز رسیده | 429 |
| `ERR_AUTH_005` | نام کاربری یا رمز عبور اشتباه | 401 |
| `ERR_AUTH_006` | دسترسی ممنوع | 403 |
| `ERR_VALIDATION_001` | ورودی نامعتبر | 422 |
| `ERR_VALIDATION_002` | فیلد الزامی وارد نشده | 422 |
| `ERR_NOT_FOUND_001` | منبع درخواستی یافت نشد | 404 |
| `ERR_CONFLICT_001` | تداخل: منبع در حال استفاده | 409 |
| `ERR_FORBIDDEN_001` | دسترسی به این منبع مجاز نیست | 403 |
| `ERR_PAYMENT_001` | درگاه پرداخت در دسترس نیست | 503 |
| `ERR_PAYMENT_002` | مبلغ پرداخت نامعتبر | 422 |
| `ERR_PAYMENT_003` | وضعیت نوبت برای پرداخت مناسب نیست | 422 |
| `ERR_FILE_001` | فرمت فایل مجاز نیست | 422 |
| `ERR_SMS_003` | تمپلیت قبلاً ارسال شده | 422 |
| `ERR_SECRETARY_001` | پلن فعلی اجازه منشی بیشتر نمی‌دهد | 422 |
| `ERR_RATE_LIMIT_001` | درخواست‌های زیاد، بعداً تلاش کنید | 429 |