Commit Graph
64 Commits
Author SHA1 Message Date
hamedandClaude Opus 4.8 c103c393f3 feat(appointment-settings): let clinics manage each member doctor's booking
The API and React components were already parameterized by doctor uuid, but 14
copy-pasted identity checks limited every endpoint to "the doctor themselves or
an admin", so a clinic owner could not touch a member doctor's booking setup.

- Replaces those 14 checks with one denyDoctorAccess() that also admits the
  owner of a clinic the doctor belongs to, and a member doctor holding the
  clinic's appointment_settings permission (view for GET, update for writes).
  A doctor's own settings short-circuit before any permission lookup.
- Moves ScheduleSection and its tabs out of DoctorDetailPage into
  components/schedule/ScheduleSection.tsx so the doctor panel and the new
  clinic page render the same module instead of one page importing another.
  Pure relocation — no logic changed.
- Adds ClinicAppointmentSettingsPage: one tab per clinic doctor, each rendering
  that same section. The tab wrapper is keyed by doctor uuid so in-progress
  schedule edits cannot leak onto the wrong doctor.
- insurance-pricing accepts an optional doctor_uuid (query on GET, body on PUT)
  under the same access rule, so the visit-price card works inside the clinic
  tabs. Fixes saveInsurancePricing calling getInsurancePricing with the wrong
  argument by extracting the shared pricingPayload().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 10:02:27 +03:30
hamed 1779e0d6de feat(secretary): implement multi-doctor assignment for clinic secretaries
- Added functionality to assign a single secretary to multiple doctors within a clinic, allowing for scoped access to appointments.
- Introduced `SecretaryService` to handle the logic for assigning and syncing doctors for a secretary.
- Updated `SecretaryController` to support multi-doctor assignment via new endpoints and modified existing ones.
- Enhanced `DoctorSecretary` entity to include secretary UUID in its serialized output.
- Implemented repository methods to facilitate the retrieval and management of doctor-secretary relationships.
- Adjusted appointment filtering in `MyAppointmentsController` to ensure secretaries only see appointments for assigned doctors.
- Created tests to validate the new multi-doctor assignment functionality and appointment access restrictions.
- Updated frontend components to support multi-select for doctors in the secretary management UI.
2026-07-18 08:49:04 +03:30
hamedandClaude Fable 5 49b2ca60d7 feat(appointment): log cancellation events and show them in a Timeline
Introduce the first per-appointment event system. On cancel (via the status
or general update endpoints) an AppointmentEvent (type=cancelled, «نوبت لغو
شد») is recorded with the actor, cancel time, and an optional cancel_reason,
plus a warning-level app_log entry. New GET /appointment/{uuid}/events
returns the ordered event list. The admin appointment detail page renders a
Timeline section and the cancel dialog now collects an optional reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 11:13:23 +03:30
hamed a0ddb4c0d1 feat: add visit price requirement feature
- Introduced a new boolean flag `require_visit_price` in the `EntityInsurancePricing` to enforce visit price for appointments.
- Updated the appointment creation endpoints to validate `visit_price_rials` based on the new flag.
- Added `visit_price_rials` field to the `Appointment` entity to store the visit price.
- Enhanced the `PatientService` to validate visit price during session creation.
- Updated API documentation to reflect changes in appointment and insurance pricing.
- Implemented a new service `VisitPriceRequirementResolver` to determine if a visit price is required for a doctor based on their pricing settings.
- Added migrations to update the database schema for the new fields.
2026-07-16 19:44:30 +03:30
hamedandClaude Opus 4.8 e00fe997f9 feat: section-based service picker + per-appointment duration override (service mode)
Service-booking mode now selects services by section like slot mode:
appointment-booking-services returns service_section per item; ServiceSlotPicker
groups by section (SearchableSelect), accumulates picks across sections into a
removable 'section -> service' chip list.

Secretaries can override a service's duration for a single appointment without
changing the service default: appointment-service-slots accepts durations[uuid]
and both create endpoints accept service_durations; the override drives total
duration and slot_end. Online (patient) booking is unaffected — it never sends
overrides. Backend + frontend tests and docs updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 10:41:15 +03:30
hamedandClaude Opus 4.8 4642506c0f feat: support multiple services per appointment (checkbox selection)
Appointments could only reference a single service (ManyToOne). Add an
appointment_service_items join table (ManyToMany) so an appointment can
carry several services; the first stays the primary service_item for
backward compatibility, and toArray now also returns service_items[].

Both create endpoints (my/appointment, admin/appointment) accept
service_item_uuids[] and attach all of them. A new duration_from_services
flag gates the slot_end recompute: service-booking mode sends it true
(slot_end = start + Σ durations); slot mode omits it so the manual end
time is preserved. The admin endpoint previously ignored services entirely.

Frontend: in slot mode the single service dropdown becomes a checkbox list
filtered by the selected section (multi-select); service mode sends the
duration flag. Migration + backend/entity tests + docs updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 10:10:47 +03:30
hamed ac4a430564 feat(appointment): add booking services endpoint and update security configuration for public access 2026-07-15 23:48:38 +03:30
hamed 2df16c6d29 feat(appointment): implement immutable booking mode after first save and update API documentation 2026-07-15 23:33:00 +03:30
hamed 5937f7e176 feat: add service-based booking mode to appointment scheduling
- Introduced a new booking mode in WeeklySchedule to support service-based appointments.
- Updated SlotCalculatorService to calculate available start times based on selected service durations and buffer times.
- Enhanced AppointmentController to handle service items during booking, calculating slot_end on the server side.
- Implemented validation to ensure at least one bookable service exists for doctors in service mode.
- Added new API endpoint to retrieve available appointment slots based on selected services.
- Updated MyAppointmentsController to accept service items during appointment creation.
- Modified ServiceItem entity to include a bookable flag, allowing services to be marked for scheduling.
- Created migration to add bookable column to service_items table.
- Added tests for service-based slot calculations and validation logic.
2026-07-15 23:15:45 +03:30
hamed 13bf9453e5 feat(appointment): ensure existing profile name is used over modal input for patient name 2026-07-15 20:33:15 +03:30
hamed d91867bd63 fix(appointment): resolve issue with reserved slots incorrectly shown as available 2026-07-15 19:13:27 +03:30
hamed de6fe1bd56 feat(appointment): update patient identification to use national code from profile and enhance appointment creation logic 2026-07-15 18:46:56 +03:30
hamed 5d5089244b feat: add mobile-based patient lookup for appointment booking
- Implemented a new endpoint `/api/v1/my/appointment/patient-lookup` to search for patients by mobile number before booking an appointment.
- Updated the `NewAppointmentModal` component to utilize the new patient lookup feature, allowing for direct booking if the patient is found with a national code.
- Enhanced the appointment booking form to handle mobile input normalization and display relevant fields based on the search results.
- Added tests for the new patient lookup functionality, ensuring proper behavior for found and not found cases, as well as validation for mobile input.
- Updated sidebar tests to reflect changes in the sidebar component structure and functionality.
2026-07-15 18:27:29 +03:30
hamedandClaude Opus 4.8 5548d79d4c feat(appointment): identify admin-booked patient by national code
Admin-side booking (POST /api/v1/my/appointment and
/api/v1/admin/appointment) resolved the patient User by mobile only, so
one person booked under two mobiles produced two User rows — and two
case-files, since PatientRecord is keyed on user_id. National code is the
real unique identity (User.national_code is already unique); a person may
have several mobiles.

Booking now requires + validates patient_national_code and resolves the
patient national-code-first (then mobile) via a shared PatientResolver, so
the case-file stays unique per national code even across mobiles. Reusing a
mobile already bound to a different national code returns 422
ERR_PROFILE_MOBILE_TAKEN. The admin create form and NewAppointmentDrawer
gain a national-code field and send it; both had a dead patient-picker URL
(/api/v1/patient) fixed to the real /api/v1/patients, whose payload already
carries user_national_code for autofill.

Docs (appointment.md, admin.md) and tests updated; new
AppointmentNationalCodeTest covers success, single-file reuse, missing,
invalid, and identity-conflict cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:11:40 +03:30
hamedandClaude Fable 5 f736e4acd8 feat(appointments): edit page + filters modal — phase C2/C3
- AppointmentEditPage (edit.pdf): full-page edit of بخش/سرویس/پرسنل, Jalali
  date + start/end time, deposit, status and notes; hydrates from
  GET /appointment/{uuid} and saves through the general PATCH with the
  optimistic-lock version. Routed at /admin/appointments/:uuid/edit (the
  actions-menu ویرایش target).
- AppointmentFiltersModal (filter-desktop.pdf): name/national-code search,
  بخش/سرویس selects, six status checkboxes (لغو شده covers both cancel
  reasons), gender radios, حذف همه reset. Filtering is client-side over the
  loaded day via the pure applyAppointmentFilters; toolbar gains the filter
  button with an active indicator.
- /my/appointments rows now include patient_national_code and patient_gender
  so the filters have data to match on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 00:02:22 +03:30
hamedandClaude Fable 5 4678739d15 feat(appointments): backend for clinic workflow (Figma نوبت‌ها) — phase A
Extend Appointment for the clinic-facing appointments area:

- New nullable relations service_section/service_item/staff (بخش/سرویس/پرسنل)
  plus deposit_required/deposit_amount_rials (بیعانه) and is_reserve.
- New statuses following_up (در حال پیگیری) and salon (سالن) with day-of
  transition rules; reserve entries never occupy a slot (several reserves may
  share one day), enforced in refreshActiveSlotKey.
- rescheduleTo(slotStart, slotEnd, isReserve) keeps active_slot_key consistent
  for جا به جایی and reserve transfers.
- New PATCH /api/v1/appointment/{uuid}: partial update covering edit, slot
  move (409 on taken slot, race backstop on the unique key), reserve toggle,
  patient swap (جایگزینی) and optional status transition; optimistic lock via
  version like the status endpoint.
- POST /my/appointment now accepts the workflow fields and is_reserve
  (day-level entry: no past-slot rule, no atomic slot booking); GET
  /my/appointments gains reserve=1 and returns the new fields per row.

Migration Version20260713195434 (+ mirrored on db_test). Docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 23:37:32 +03:30
hamed a4a17bf8c7 feat: add patient appointment interface and endpoints
- Introduced `PatientAppointment` interface to define appointment structure.
- Implemented `findByUserAndDoctorIds` method in `AppointmentRepository` to retrieve appointments for a user filtered by doctor IDs.
- Added `acceptedDoctorIdsByClinic` method in `ClinicDoctorInvitationRepository` to get accepted doctor IDs for a clinic.
- Created new endpoint in `PatientController` to fetch appointments for a patient, ensuring only relevant doctors' appointments are displayed.
2026-07-13 10:15:44 +03:30
hamed 0750bc9812 feat: add multi-city representation support and domain context resolution
- 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.
2026-07-09 07:26:58 +03:30
hamed 34d521434f feat(payment): implement expiration handling for appointment payments 2026-07-03 09:53:55 +03:30
hamedandClaude Opus 4.8 4b80616a17 refactor(errors): centralise legacy string error codes in ErrorCodes (M21)
~27 ad-hoc error codes (SLOT_TAKEN, USER_NOT_FOUND, VALIDATION, …) were raw
strings, so ErrorCodes::message() returned the "unknown" fallback for them.
Register all 14 distinct codes as constants with their messages and replace the
raw usages across AdminApiController, MyAppointmentsController, CategoryController,
PreRegistrationController and ClinicInvitationController.

Wire values are kept identical (verified no consumer — admin SPA, nobat724_front,
tauri — switches on these strings), so this is backward compatible.

Regression: tests/Shared/ErrorCodesTest (wire values preserved + message resolves).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:12:23 +03:30
hamedandClaude Opus 4.8 96a86dbfed fix(db): business-key unique constraints (M16-M19)
Add unique constraints (one migration, no dup data in either DB):
- users.email, users.national_code (M16) — NULLs still allowed.
- payments.gateway_token (M17).
- date_overrides (doctor_id, date) (M18) — was a non-unique index.
- financial_breakdowns (payment_id, source) (M19) — anti double-accounting.

Regression: tests/Database/UniqueConstraintsTest (4 duplicate-insert cases).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:03:54 +03:30
hamedandClaude Opus 4.8 fe6383314e fix(security): enforce ownership on 4 IDOR read/bind endpoints (M2-M5)
- M2 GET /insurance/{id}: was unguarded; now owner-or-admin (403 otherwise) —
  stops reading another doctor's negotiated price by id enumeration.
- M3 GET /clinic-pro/doctor-address/{id}: add the same owner/admin check the
  sibling PATCH/DELETE already had.
- M4 POST/PATCH /service-item: staff_uuid must belong to the caller's tenant
  (entity_type/entity_id) → 422; stops binding another tenant's staff.
- M5 appointment-settings list endpoints (date-override/holidays/
  available-locations): add the per-doctor ownership check the sibling
  single-record endpoints already enforce.

Regressions (6 negative cases fail without the fixes):
DoctorInsuranceOwnershipTest, DoctorAddressOwnershipTest,
ServiceItemStaffOwnershipTest, AppointmentSettingsListOwnershipTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 20:17:52 +03:30
hamedandClaude Opus 4.8 aa87b4a9cb fix(db): prevent double-booking a slot via unique active_slot_key (H2)
A non-unique index on (doctor_id, slot_start) plus a count-then-insert check
left a TOCTOU race: two concurrent requests could both pass isSlotTaken and
both insert. wrapInTransaction alone doesn't stop the phantom under InnoDB
REPEATABLE-READ.

Add a nullable, unique active_slot_key on Appointment = "doctorId:slotStart"
while the booking occupies the slot (pending/confirmed — in lockstep with
isSlotTaken); NULL once expired/completed/no_show/cancelled (NULLs don't collide
in a MySQL unique index, so released slots rebook freely). bookAtomically now:
catches the unique violation -> SlotTakenException, and expires lapsed pendings
in-transaction so the ~1-min window before the expiry cron doesn't wrongly block
rebooking. All three booking paths (online / my / admin) routed through it.

Migration backfills one row per (doctor, slot) — the latest id — so the index
builds even on dirty historical data without destructively cancelling bookings.
(Backfill surfaced a real pre-existing double-booked slot in dev data.)

Regression: tests/Appointment/SlotUniquenessTest. Adjusted the expiry-service
test fixture to use distinct slots (one live booking per slot is now enforced).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:04:54 +03:30
hamedandClaude Opus 4.8 c084571bf0 fix(security): enforce doctor scope on POST my/appointment (H1)
createAppointment only checked the caller held an allowed role, then booked
onto whatever doctor_uuid the request named — a doctor could book onto any
other doctor's calendar, a clinic onto doctors outside it, a secretary outside
their scope. Add canBookForDoctor(): doctor→own only, clinic→member doctors,
secretary→active scope + appointments.create permission, admin→any.

Regression: tests/Appointment/BookingScopeTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 18:52:23 +03:30
hamed 8b96753df6 perf(db): add missing indexes (appointment expiry, session dates, user status)
- appointments(status, expires_at): the per-minute expiry scheduler filtered on
  these with no covering index
- patient_sessions(created_at): dashboard revenue range scans
- users(status): admin user-list filter

PatientRecord(entity_type,entity_id) already covered by its unique constraint;
UserActiveContext.user_id is the PK — neither needed a new index.
2026-06-28 17:13:44 +03:30
hamed e1740462b5 perf(appointment): batch-fetch pending payments in expiry loop (fix N+1)
AppointmentExpiryService ran one findPendingByAppointment query per expiring
booking. Add PaymentRepository::findPendingByAppointments (one IN query keyed
by appointment id) and use it. Test covers expiry + payment cancellation for
several appointments at once.
2026-06-28 17:10:32 +03:30
hamed 093293004a fix(security): enforce ownership on GET date-override (IDOR)
getOverride leaked any doctor's override to any authenticated user; add the
owner-or-admin check (matching the update/delete endpoints) + regression test.
2026-06-28 16:48:41 +03:30
hamed 90736c8149 fix(security): enforce ownership on GET weekly-schedule (IDOR)
getSchedule returned any doctor's schedule to any authenticated user — the
mutation endpoints (update/delete) already checked owner-or-admin but this GET
did not. Add the same check + regression test (fails without the fix).
2026-06-28 16:47:05 +03:30
hamedandClaude Opus 4.8 05fde81320 fix(orm): declare repositoryClass on all entities with a custom repository
On prod (opcache.preload + prod container), getRepository(Entity::class)
returned Doctrine's default repository instead of the custom one when the
entity's #[ORM\Entity] had no repositoryClass — so custom finders like
DoctorSecretaryRepository::findAllActiveBySecretary threw BadMethodCallException,
making /oauth/userinfo return 500 after login. Declare repositoryClass explicitly
on all 25 affected entities.

Also add app:create-admin command (create/promote a ROLE_ADMIN user by mobile).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 16:19:07 +03:30
hamed 9603b702c1 feat: implement domain guard for commission calculation and enhance representation dashboard
- Added domain guard in CommissionService to ensure commission is calculated only when the appointment is booked under the same representation as the doctor.
- Updated RepresentationController to filter statistics by representation, ensuring accurate data is shown for each representative.
- Introduced new endpoints for the representation dashboard to provide summary statistics, doctor performance, and financial reports.
- Created new pages for RepresentationFinance and RepresentationSettlement to display financial data and allow for settlement requests.
- Added migration to include booking_representation_id in appointments for tracking the representative under which the appointment was booked.
2026-06-24 16:14:41 +03:30
hamed e0abaf5c0c feat(appointment): enforce mandatory patient national code and gender with validation 2026-06-24 12:32:58 +03:30
hamedandClaude Opus 4.8 af881231d0 fix: store appointment address from schedule and auto-add patient to clinic
Appointments now persist address_id resolved from the weekly-schedule
session (location_id) across all booking paths (online, secretary, admin).
On confirm, the patient is added to the clinic owning that address, or to
the doctor's single clinic as fallback. Weekly-schedule create/update now
requires location_id on every active session. PatientSession exposes
doctor_uuid/doctor_name so clinic records show which doctor each visit is for.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:10:36 +03:30
hamed a8d36d7455 feat(user-profile): add avatar upload functionality and update UserProfile entity 2026-06-19 10:19:47 +03:30
hamedandClaude Opus 4.8 28011160a2 feat(appointment): include doctor specialties and clinic address in toArray
The appointment detail view needs the doctor's specialty and the clinic
address/phone/map, which toArray didn't return. Add doctor.specialties[] and
a top-level `address` (the doctor's first address via DoctorAddress::toArray —
address, telephone, map). Additive only; existing keys unchanged. Docs updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 12:01:32 +03:30
hamedandClaude Opus 4.8 492a7df989 feat(payment): canceled status, manageable origin allowlist, CORS subdomains
Unify and harden the payment flow (same API for the main site and all
consumer sites; per-client difference is only frontend_address).

- Payment gains STATUS_CANCELED. Gateways distinguish user-cancel from
  failure (Mellat ResCode=17, SEP CanceledByUser, mock cancel=1) via a new
  PaymentVerifyResult::canceled flag; callback sets canceled vs failed and
  skips the circuit-breaker on cancel.
- Expiry job now cancels the pending payment when a booking lapses
  (AppointmentExpiryService + PaymentRepository::findPendingByAppointment).
- frontend_address allowlist is read from the payment_allowed_frontend_hosts
  site setting (manageable via PATCH /api/v1/admin/settings), falling back to
  the ALLOWED_FRONTEND_HOSTS env var — so a new consumer site needs no code
  change.
- .env: broaden CORS_ALLOW_ORIGIN to city subdomains (*.localhost /
  *.clinic-pro.ddev.site) and add yazd-nobat.localhost to ALLOWED_FRONTEND_HOSTS.
- Update docs/api/payment.md and docs/api/admin.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 10:06:43 +03:30
hamedandClaude Opus 4.8 45242a3128 feat(rating): multi-dimensional ratings, rich comments, eligibility guard
Rebuild the doctor rating/review system to power the public site's rich
review UI, and restrict who may submit.

Ratings:
- Rate entity holds five 0–100 dimensions (waiting time, diagnosis
  accuracy, behaviour, cleanliness, expertise) instead of a single score.
- GET /rate/{uuid} returns aggregate {point, satisfaction, averages[]}.
- POST /rate upserts all five dimensions and returns the new aggregate.

Comments:
- Comment gains parent/replies (threaded) and a rich toArray with author,
  like_status (like/dislike counts + current user's vote) and nested
  approved replies. POST /comment accepts {comment, parent}.
- Likes are directional (value 1=like, -1=dislike) with toggle/replace;
  POST /like/{uuid} returns like_count/dislike_count/current_user_like.

Eligibility:
- Only a user with a confirmed appointment in the last 30 days may rate or
  comment (AppointmentRepository::hasRecentConfirmed); otherwise
  403 ERR_RATING_NOT_ELIGIBLE. New GET /rate/{uuid}/eligibility for the UI.
- security.yaml: narrow the public rate pattern so /eligibility stays auth'd.

Also updates admin rates listing to the new dimensions and the rating/admin
API docs. Includes migration for the new columns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 00:25:59 +03:30
hamedandClaude Opus 4.8 cc182e48f9 refactor(scheduler): use the recipe's default stateful schedule
Consolidate onto the symfony/scheduler recipe's App\Schedule (stateful +
processOnlyLastMissedRun, so missed runs after downtime still execute)
instead of a separate provider. Add the every-1-minute
ExpireAppointmentsMessage there, point scheduler_default at
schedule://default, and drop the redundant ExpireAppointmentsSchedule.
debug:scheduler shows the trigger registered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 22:47:38 +03:30
hamedandClaude Opus 4.8 71ab8892c9 feat(appointment): auto-expire unpaid bookings via Symfony Scheduler
Run the 15-min payment-expiry every minute through Symfony Scheduler so
unpaid pending bookings flip to expired without a system crontab. Install
symfony/scheduler; extract the expiry logic into AppointmentExpiryService
(reused by the existing command); add ExpireAppointmentsMessage + handler
and an #[AsSchedule] provider (RecurringMessage::every 1 minute); wire a
scheduler_default transport in messenger.yaml. Slots already free
just-in-time via isSlotTaken, so this only syncs the DB status.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 22:43:50 +03:30
hamed daf2bd4b91 Merge branch 'feat/appointment-lock-patient'
# Conflicts:
#	src/Appointment/Controller/AppointmentController.php
2026-06-15 18:29:25 +03:30
hamed 6e7101f113 Merge branch 'fix/past-slots-unavailable'
# Conflicts:
#	docs/api/appointment.md
2026-06-15 18:28:06 +03:30
hamedandClaude Opus 4.8 089badbb5a feat(appointment): expire pending bookings past their payment window
Add findPaymentExpired (pending with expires_at < now) and have the
cancel-expired command flip both payment-expired and slot-time-passed
pendings to expired (deduped). Run it on a schedule (e.g. every minute:
* * * * * php bin/console app:cancel-expired-appointments) to free locks
held by unpaid bookings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:58:28 +03:30
hamedandClaude Opus 4.8 aa2e46dfdb feat(appointment): book with patient data, TTL lock, atomic conflict
book() now accepts for_self plus patient_* fields: for_self fills the
patient from the paying user's profile, otherwise patient_name/mobile are
required (422 if missing) and the rest are stored. Every booking starts
pending with a 15-minute expires_at. Persisting goes through
bookAtomically (re-check inside a transaction) so two concurrent requests
for the same slot can't both win — the loser gets 409.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:56:56 +03:30
hamedandClaude Opus 4.8 14ac01d597 feat(appointment): treat expired pending bookings as free in isSlotTaken
A slot is taken only if confirmed, or pending with a still-valid lock
(expires_at null or in the future). An expired pending booking no longer
blocks the slot even before the cron flips it to expired.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:54:18 +03:30
hamedandClaude Opus 4.8 bff7001f49 feat(appointment): add expires_at and patient fields to Appointment
Add a 15-minute payment TTL (expires_at) plus patient_name/mobile/
national_code/gender/reason columns so a booking can hold a slot
temporarily and record a patient distinct from the paying user. New
markPendingWithTtl() sets the lock; transitioning out of pending clears
expires_at. All columns nullable (migration added).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:53:27 +03:30
hamedandClaude Opus 4.8 4216e67d68 fix(appointment): mark past-time slots as unavailable
is_available only checked whether a slot was booked, so on today's date
slots whose start time had already passed (e.g. 10:00 when it is 12:00)
still showed as bookable until the POST /appointment 422. Treat a slot
with start < now as unavailable in getAllSlotsWithAvailability, and drop
past slots in filterBookedSlots so getAvailableSlots agrees. Future days
are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:48:50 +03:30
hamedandClaude Opus 4.8 f443beb615 feat(appointment): public month-availability endpoint
Add GET /api/v1/appointment-settings/month-availability/{doctorUuid}
?year=&month= (Gregorian) returning disabled_dates / enabled_dates for
the month plus the doctor's online_booking flag and window. Lives in
AppointmentController (per-method guards) so it is genuinely public —
the class-level IsGranted on AppointmentSettingsController would have
forced auth. Whitelisted in security.yaml (firewall + access_control).
Uses SlotCalculator::hasAnyAvailability per day, so holidays, closed
overrides, non-working days and out-of-window dates all come back
disabled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:23:12 +03:30
hamedandClaude Opus 4.8 f82d92b3ed feat(appointment): enforce booking window in slot calculation
buildAllSessions now rejects dates that are in the past, beyond the
doctor's booking window (today + value week|month), or when online
booking is disabled — gating overrides and weekly schedule alike. Add a
public hasAnyAvailability() for the month-availability endpoint. Window
config is read from the schedule meta, falling back to defaults.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:18:00 +03:30
hamedandClaude Opus 4.8 09aba878f8 feat(appointment): store online-booking window in WeeklySchedule meta
Add a reserved meta key inside WeeklySchedule.setting (no migration) holding
online_booking_enabled and a booking_window value+unit (week|month), with
sane defaults. Expose meta separately in toArray() and keep it out of the
day schedule map. setSetting now preserves meta when the day schedule is
replaced; the weekly-schedule POST/PATCH endpoints accept an optional meta.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:16:30 +03:30
hamed c662eb9b4f feat(doctor): update schedule fields in doctor API responses to reflect actual availability 2026-06-15 15:11:29 +03:30
hamed ec36eefeb8 feat: add OpenAPI tags to various controllers for improved API documentation 2026-06-15 14:54:10 +03:30