PatientController::resolveScope and TenantTagController::guardTagView only ever checked the secretary, while every write in both controllers already ran through both checkers. So an invited clinic doctor with patients.view off got 200 with an empty list where a secretary got 403 — one permission, two behaviours. No data was exposed either way; tenant scoping emptied the result. The fix is not canOrNonMember. That collapses two different situations: a membership row switched to active=false means the collaboration ended, and ClinicDoctorPermission::can() returns false for everything in that case too. Routing it through the permission gate turned the existing 404 on a single record into a 403, which confirms the record exists to someone who just lost access. ClinicRecordAccessTest caught it. isActiveMemberDenied() answers the narrower question — active member, permission off — and leaves a deactivated row to the data scope, which closes it with a 404 and discloses nothing. A test now pins that distinction so it cannot be collapsed again. Tags keep the tags.view OR patients.view rule, now for both roles. Verified live in three states: active with both off 403/403, deactivated not 403, active with patients.view on 200/200. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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
// 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.
// 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_KEYSin the subscriber, otherwise Persian digits reach the database.
Modules
| File | Domain | Endpoints |
|---|---|---|
| auth.md | Authentication — OTP, Login, JWT | 8 |
| doctor.md | Doctor profile & addresses | 11 |
| clinic.md | Clinics | 7 |
| practice-domain.md | Practice domains — a clinic's field of practice | 3 |
| treatment.md | Treatment protocols — multi-session courses on a service | 3 |
| clinic-invitation.md | Doctor invitations to clinics | 8 |
| resource.md | Resources, types, skills, pools | 16 |
| resource-calendar.md | Resource calendars, exceptions, national holidays | 9 |
| appointment-plan.md | Appointment segments and plan preview | 3 |
| appointment-availability.md | Multi-resource availability search | 2 |
| appointment-booking.md | Holds, confirmation and multi-resource occupancy | 4 |
| pricing.md | Date-ranged price lists and appointment invoices | 8 |
| appointment.md | Appointments & slot booking | 6 |
| appointment-settings.md | Weekly schedule, date overrides, holidays | 14 |
| payment.md | Payments (Mellat / Sep) | 5 |
| settlement.md | Wallet & settlement requests | 7 |
| rating.md | Ratings, comments, likes | 9 |
| secretary.md | Doctor secretaries | 5 |
| permission.md | Permission catalog — single registry of permissionable resources | 1 |
| representation.md | Representations (agents) | 6 |
| sms.md | SMS send & templates | 10 |
| blog.md | Blog posts | 6 |
| specialty.md | Medical specialties | 5 |
| insurance.md | Insurances & doctor-insurance links | 10 |
| doctor-service.md | Doctor services | 5 |
| tag.md | Blog tags | 5 |
| location.md | Provinces & cities | 10 |
| user-profile.md | User medical profile | 4 |
| 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 |