A representative could create a doctor or clinic but not finish its profile:
PATCH /api/v1/doctor/{uuid} accepted only the doctor or an admin, and the
clinic gate ran through ClinicDoctorPermissionChecker, which asks about clinic
membership — a representative is not a member. Onboarding stopped at an empty
public record.
Grant is permanent while representation_id points at the rep, and limited to
content: RepresentationEditPolicy holds ownership plus the field whitelist.
Sending a key outside it aborts the whole request with 403 and names the field,
rather than filtering the payload silently, so a rep never believes a change
saved when it did not. medical_system_code, `active` and clinic `doctors` stay
out — credential, and membership, belong to the record's owner. `active` already
has a dedicated rep endpoint.
ClinicDoctorPermissionChecker is untouched on purpose; folding a second concept
into it would give it two reasons to change.
Doctor/clinic detail responses now carry can_edit, computed by the same policy
the PATCH gate uses, so the panel reads authorization instead of re-deriving it
and drifting. Both endpoints stay public: no token means can_edit false and an
otherwise unchanged payload, which is what nobat724_front consumes.
Address endpoints follow the same policy. createAddress now resolves its target
from an explicit doctor_uuid instead of findByUser first — a representative who
also has a doctor profile was silently writing the address onto their own.
Every rep edit writes one app_log row (channel representation_edit) recording
who, what, and which field names — never values. Owner and admin edits write
nothing, keeping /admin/logs readable.
Docs corrected where they already disagreed with the code: 403/404 error codes
on both PATCH routes, a non-existent "cannot delete the last clinic address"
409, and the missing gallery-size 422.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
A full role-by-role sweep (9 roles x 18 endpoints against the running app) showed
the addresses toggles in the owner's permission form controlled nothing. Grep
confirms it: no gate anywhere referenced 'addresses'. The panel's address list was
gated on appointment_settings.view instead — the same borrowed-permission pattern
already fixed for resources and treatment.
GET /api/v1/addresses now gates on addresses.view.
The resource drops to view-only. Creating, updating and deleting an address in
ClinicController is explicitly owner-or-admin
($clinic->getUser()->getId() !== $user->getId()), so those three actions could
never be delegated to a secretary or an invited doctor no matter what the form
said. Both role defaults narrow to ['view' => true] to match, and stored JSON
keeps its old keys harmlessly since merge only reads registry keys.
This widens secretary access: addresses.view defaults to true while
appointment_settings.view defaults to false, so secretaries who could not list
addresses now can. That is deliberate and costs no confidentiality — the same
addresses are already served anonymously from
GET /api/v1/clinic/{uuid}/addresses, which is whitelisted in security.yaml.
Verified live in three states: default 200, addresses.view off 403, and
addresses off with appointment_settings on still 403, proving the borrow is gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
clinic.md's default envelope is regenerated from the running app, so it now shows
all 17 resources instead of 13, including services with its full create/delete
actions. Both role docs point at permission.md for the shared registry and spell
out the merge rule that makes new resources work on existing rows: deleting a key
means "take the default", not "deny" — denying requires an explicit false.
A systematic sweep over every gated route with all permissions off found two
places where the docs claimed enforcement that does not exist:
- GET /api/v1/subscription/my returns 200 with every permission off. Only trial
is gated.
- ServiceCatalogController has no gate at all.
Both are pre-existing and both are left as-is rather than half-fixed: their
endpoints are also consumed by the booking and subscription flows, where a hard
gate would break secretaries who legitimately need them. The docs now say so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A practice domain is the field a clinic operates in — beauty, dentistry —
and unlike Specialty it is configuration, not a label: treatment workflows
will bind to its code, so the code is immutable once created and only a
platform admin can mint one. A clinic that has not chosen a domain keeps
behaving exactly as it does today.
Assignment reuses PATCH /api/v1/clinic/{uuid} rather than adding a second
endpoint. An unknown domain uuid is rejected instead of silently dropped,
because a lost selection would only surface at the first protocol-driven
booking.
Also corrects ADR-0003: resource occupancy does not in fact guard the panel
booking path, which writes appointments.resource_id and no occupancy row at
all, so the doctor slot key cannot simply be dropped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The clinic-member-doctor permission system (ClinicDoctorPermission) lagged the
secretary system: only 6 resources, enforced in ~6 places, dead toggles
(services.update never checked), and a sidebar showing just appointments+patients.
Bring it to parity so a clinic owner can control exactly what each member doctor
does — while an independent doctor stays completely unrestricted.
Coverage: add insurances, addresses, inventory, tags, staff, discounts, sms to
ClinicDoctorPermission::DEFAULT_PERMISSIONS + DoctorPermissionsModal
(subscription/clinic_doctors stay owner-only by design).
New App\Clinic\Security\ClinicDoctorAccessChecker (parallel to
SecretaryAccessChecker):
- denyUnlessGranted(user, resource, action): 403 only for a clinic-member doctor
in the clinic context; owner/admin/secretary/independent-doctor pass through.
- memberClinicId(user): resolves the member doctor to the CLINIC's tenant so the
role-based controllers (Inventory/Tag/Staff/Discount/Sms) stop showing them
their personal tenant in clinic context.
Enforcement wired into 10 controllers alongside the existing secretary gates:
ClinicService (services), Insurance (insurances), Patient (patients+payments),
Staff, Discount, Inventory, Tag, SmsWallet, Payment, PaymentMethod.
Frontend: the guest-doctor sidebar branch now exposes every permitted resource
(gated by can()) plus a «تنظیمات» entry; both settings navs (PurchaseSubscription
Sidebar + SETTINGS_MENU) are now permission-filtered for a scope=clinic doctor,
not just secretaries; my-payments route gets the missing payments permission.
CRUD-button gating already applies (usePermissions is role-agnostic).
Tests: ClinicDoctorPermissionEnforcementTest (member denied/allowed +
independent-doctor-unrestricted); guest-doctor sidebar gating. Backend 375 pass,
frontend 503 pass. docs/api/clinic.md updated with the full resource set +
enforcement notes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Removed the "دکتر" prefix from doctor names in various components and API responses to ensure consistency and clarity.
- Updated the AppointmentDetailPage, CommentsPage, DashboardPage, RatingsPage, SecretariesPage, and other relevant files to reflect the changes in doctor name formatting.
- Adjusted API documentation to align with the new naming conventions.
- Implemented validation to prevent the creation of clinics without a name and restricted users to a single clinic.
- Added tests to verify that doctor names are stored without titles and that clinic creation adheres to the new validation rules.
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>
- Implement DoctorPermissionsModal for managing doctor permissions in clinics.
- Create usePermissions hook to handle user permissions context.
- Add migration for clinic_doctor_permissions table with default permissions.
- Develop ClinicDoctorPermissionController for handling permissions API.
- Create ClinicDoctorPermission entity to manage permissions data.
- Implement ClinicDoctorPermissionRepository for database interactions.
- Add ClinicDoctorPermissionChecker for permission validation logic.
- Write tests for clinic doctor permissions functionality.
- Removed MyClinicPage and redirected its functionality to a new ClinicDoctorsPage.
- Created ClinicDoctorsManager component for managing doctors and invitations within the settings layout.
- Updated backend permissions to allow clinic owners to detach doctors, alongside admins.
- Adjusted API documentation to reflect new permission structure.
- Updated tests to cover new functionality and permissions.
- Modified sidebar and settings menu to reflect the new structure and role-based visibility.
- 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.
- Updated the clinic API response to include new fields: title, phone, logo, images_clinic, doctors_count, city, state, 24_7, and field_working_days.
- Modified the ClinicController to fetch and include city and state information based on the clinic's address.
- Refactored the toListArray method in the Clinic entity to accept city and state parameters.
- Added a new method in the ClinicRepository to retrieve city and province names for each clinic based on their address.
- Added `clinic_id` and `type` fields to `DoctorAddress` entity to differentiate between personal and clinic addresses.
- Updated constructor to support creation of addresses for both doctors and clinics.
- Modified repository methods to handle new address types and added methods for counting and finding addresses by clinic.
- Implemented migration to update the database schema accordingly.
- Removed deprecated endpoint for creating addresses from clinics and updated related controller methods.
- Added new endpoints for managing clinic addresses, including CRUD operations.
- Updated frontend components to handle new address types and display accordingly.