Commit Graph
13 Commits
Author SHA1 Message Date
hamedandClaude Opus 4.8 7fa4b55d3f fix(security): unique payment reference_id, reject replayed callbacks (H3)
reference_id (the gateway's settled-transaction ref) was not unique, so the
same successful callback — or a RefNum replayed onto another order — could
credit twice. Add a unique index (NULL until success, so pending/failed rows
don't collide) and an application-level pre-check in the callback that fails the
payment if the reference already belongs to another order. The unique index is
the hard backstop behind the check.

Regression: PaymentCallbackAmountTest::testReplayedGatewayReferenceIsRejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:08:26 +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
hamedandClaude Opus 4.8 ae06498a96 fix(security): verify gateway-confirmed amount in payment callback (C1)
The callback marked an order success on any verify-ok result without comparing
the gateway-settled amount to the amount charged. SEP returns AffectiveAmount;
an underpayment or a replayed RefNum from a cheaper order would confirm the
expensive order. Now reject (status=failed, no activation) when the gateway
reports an amount that mismatches the stored amount_rials. Gateways that don't
report a settled amount (Mellat binds it server-side) skip the check.

MockGateway now echoes mock_amount so the guard is exercisable in tests.
Regression: tests/Payment/PaymentCallbackAmountTest (underpayment rejected,
matching amount succeeds).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 18:49:33 +03:30
hamedandClaude Opus 4.8 6bd49c2d3e fix(security): make commission_percent & active admin-only on PATCH representation
A representation editing its own record could raise its own commission or
self-activate (privilege escalation). Restrict both fields to ROLE_ADMIN and
range-check commission (0–100). Owner can still edit name/city/bank.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 17:36:48 +03:30
hamed e456809b55 fix(devops): repair phpstan config so static analysis runs again
- container_xml_path -> containerXmlPath (phpstan-symfony v2 rename); the old
  key made phpstan abort with an invalid-configuration error, so analysis
  silently never ran
- add the missing tests/doctrine_object_manager.php loader
- drop a stale ignoreErrors pattern

phpstan now runs and surfaces 42 pre-existing level-5 errors (tracked separately).
2026-06-28 17:17:46 +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 372bea4849 test(orm): regression — every entity with a custom repo declares repositoryClass
Auto-discovers each mapped entity that has a sibling <Entity>Repository class
and asserts getRepository() returns it (not Doctrine's default). Catches the
prod-only opcache.preload bug class that broke /oauth/userinfo.
2026-06-28 16:53:40 +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 30c5fbe98c test: functional test infrastructure (ApiTestCase + db_test + JWT helpers)
- .env.test points at db_test (doctrine when@test appends _test suffix)
- ApiTestCase: WebTestCase base with createUser/jwtFor/authJson helpers
- InfraSmokeTest: proves boot + DB + JWT auth pipeline works (/health 200,
  unauth /oauth/userinfo 401, JWT-auth userinfo 200)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 16:44:05 +03:30
hamedandClaude Opus 4.8 89191eee57 feat: insurance & medical billing system (6 phases)
Multi-tenant insurance contracts, service coverage, versioned tariffs,
invoice calculation, and insurance claims with debt reporting.

- TenantInsurance: per-tenant insurance contracts (coverage/franchise/ceiling,
  versioning, soft-deactivate) + active guard
- ServiceItem.insuranceCovered + TenantServiceCoverage per-service overrides
- Tariff: versioned yearly tariffs with fallback to ServiceItem price
- Billing domain: Money/ShareBreakdown VOs, BillingCalculator (unit-tested),
  Invoice/InvoiceItem aggregate, InvoiceService.createFromSession
- Claim/ClaimItem with state machine (pending->submitted->approved/rejected->paid),
  ClaimService, insurance-debt report
- ClaimSubmitterInterface + ManualClaimSubmitter (future insurance API ready)
- Admin UI: insurance-pricing page, claims page, service tariff modal,
  service insurance toggle; routes + sidebar entries
- Architecture doc + billing/insurance/clinic-services API docs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 15:05:24 +03:30
hamed de1a78a235 feat: Implement SMS sending functionality with KavehNegar and Rangineh providers
- Add SendSmsMessage class for encapsulating SMS message data.
- Create KavehNegarProvider and RanginehProvider classes implementing SmsProviderInterface for sending SMS.
- Implement SmsLogRepository and SmsTemplateRepository for managing SMS logs and templates.
- Develop SendSmsHandler for handling SMS sending messages.
- Create SmsService to manage SMS dispatching and logging.
- Add UserProfileController for managing user profiles with CRUD operations.
- Implement UserProfile entity and repository for user profile data management.
- Update symfony.lock and bootstrap.php for project dependencies and environment setup.
2026-06-09 22:00:34 +03:30