Section 12 and the fifth closing rule: changing a price never changes an already-booked appointment. The pricing chain already existed and worked. Two things were missing. Tariff only carries a year, so a rate change starting in Mehr could not be expressed — PriceList now takes an explicit date range and Tariff remains the layer beneath it. And an appointment stored a single number, so after a price change or a discount nobody could say what those 2,400,000 rials were made of. Price resolution walks four layers per service and takes the first hit: branch override, then the covering price list, then the yearly tariff, then the service's own price. The last one is the guarantee that a date no list covers still returns a price rather than zero or an exception. breakdown.sources reports which layer answered, so a surprising number can be traced instead of guessed at. Two calculation decisions worth stating. Tax is computed on the patient's share, not the gross — a patient does not pay tax on the portion the insurer covers. And a discount larger than the amount floors the total at zero rather than going negative, because a negative balance would mean the clinic owes the patient money, which nothing downstream is built to mean. A branch-specific list deliberately does not count as overlapping a general one; it takes precedence instead. Treating them as a conflict would have made per-branch exceptions impossible to express. Lists have no effect until activated, so drafting next quarter's prices cannot disturb today's. PriceSnapshot has no setters and a unique key on appointment_id: a snapshot that can be edited is not a snapshot, and two invoices for one appointment would be two truths. Corrections are a new row plus voiding the old one. Invoices are written during confirm with the prices of that moment — computing later would let a rate change between booking and invoicing produce a different number, which is exactly what rule five forbids. 12 tests. The one that matters is testBookedAppointmentKeepsItsOriginalInvoiceAfterAPriceChange: book, double the service price, watch quote return the new number while the appointment's invoice returns the old one. Without it rule five is only a claim. 1220 tests / 3551 assertions. phpstan back at its 14-error baseline. Frozen slot contract green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
133 lines
6.2 KiB
Markdown
133 lines
6.2 KiB
Markdown
# 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 |
|
||
| [branch.md](branch.md) | Branches (= addresses), working hours, rooms | 8 |
|
||
| [resource.md](resource.md) | Resources, types, skills, pools | 16 |
|
||
| [resource-calendar.md](resource-calendar.md) | Resource calendars, exceptions, national holidays | 9 |
|
||
| [appointment-plan.md](appointment-plan.md) | Appointment segments and plan preview | 3 |
|
||
| [appointment-availability.md](appointment-availability.md) | Multi-resource availability search | 2 |
|
||
| [appointment-booking.md](appointment-booking.md) | Holds, confirmation and multi-resource occupancy | 4 |
|
||
| [pricing.md](pricing.md) | Date-ranged price lists and appointment invoices | 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 |
|