PUT /service-item/{uuid}/segments deletes and rewrites. deleteForService issues
a DQL DELETE that runs immediately, and three validations — duration, occupancy
and constraints — only ran afterwards, while building the new rows. A rejected
request therefore deleted the service's segments and saved nothing, and the
service silently fell back to "one continuous block": different duration,
different resources, on every future appointment, with a 422 as the only clue.
Validation now happens before the delete, and the delete plus rewrite are one
transaction. A test pins it: an unknown constraint is refused and the previous
two segments are still there afterwards.
While in there, the caps the task asked for and never got: 20 segments and 10
requirements per segment. The availability engine evaluates resource
combinations per segment per requirement, so the numbers protect the search
rather than the table. They are generous — no real service reaches them, but a
bad payload does.
The plan response now carries patient_facing_minutes. "Set aside 90 minutes"
is wrong for an appointment where 40 of them are waiting for anaesthetic to
take effect, and computing it once in the backend stops each client summing it
differently.
A condition on a fact the request never supplies still evaluates to false —
that part was right — but it now logs a warning naming the policy and listing
the facts that were available. A rule that hits that line every time is
effectively switched off, and nothing said so.
A new policy version can no longer start in the past: yesterday's appointments
were priced under the previous text, and their price trace points at the
version. Backdating makes that trace describe a rule that did not exist.
require_resource errors name the policy that demanded the role. Knowing a room
is missing does not tell an operator which of ten active rules to look at.
Six operators now have a test each. An operator that compares wrongly produces
a rule that always matches or never does, and neither raises anything.
Co-Authored-By: Claude Opus 5 (1M context) <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 |
| clinic-invitation.md | Doctor invitations to clinics | 8 |
| branch.md | Branches (= addresses), working hours, rooms | 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 |
| 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 |