Files
clinicpro/config/services.yaml
T
hamedandClaude Opus 5 eebb363b9f feat(branch): branch working hours and rooms on the existing address entity
Task 01 planned a new `branches` table with `doctor_addresses.branch_id` bridging
to it. That plan was wrong: the branch already exists and is called
`DoctorAddress`. It carries name, address, telephone, coordinates, city/province
FKs and an owner (`forDoctor` / `forClinic` + `type`), and the whole system
already consumes it with exactly that meaning — `WeeklySchedule.sessions[].location_id`
points at `doctor_addresses.id`, `appointment-booking-locations` calls each row a
booking location, and nine CRUD endpoints plus four admin pages manage them.
A parallel table would mean two sources of truth for one physical place and a
branch that `location_id` never references.

So no `branches` table and no duplicate branch CRUD. Only the three genuinely
missing pieces:

- `doctor_addresses.active` / `.timezone`, both NOT NULL with a default so
  existing rows need no backfill and no current behaviour changes. `active` is
  stored only — applying it to slot calculation is task 03, since touching
  `SlotCalculatorService` is off limits in this phase.
- `branch_working_hours`, keyed to `doctor_addresses.id`. Minutes from midnight
  rather than "09:00" strings so range intersection stays arithmetic. PUT
  replaces all seven days; validation of the whole week runs before any DELETE,
  so an invalid sixth day cannot wipe the five valid ones and then answer 422.
- `rooms`, with `capacity` as concurrency (a three-bed injection room is one
  resource with capacity 3, not three resources) and a deletion-guard iterator
  so tasks 02 and 07 can add reasons without editing RoomService.

`BranchWorkingHours` first registered as an aggregate child of `DoctorAddress`;
TenantSchemaCoverageTest rejected it correctly, because that root is itself
declared global. It now carries a real tenant pair instead, derived in the
constructor from the address's `type` — a total mapping, and the address is only
ever listed in its own context, so nothing is hidden wrongly.

RoomController checks ownership explicitly rather than trusting TenantFilter:
hard isolation only applies to a *chosen* context, so a doctor who had not
selected one could PATCH another clinic's room. Caught by
RoomCrudTest::testForeignRoomIsNotFound, which failed with 200 before the fix.

35 tests, 97 assertions. Slot-mode frozen contract still green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 16:28:04 +03:30

171 lines
6.0 KiB
YAML

# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# See also https://symfony.com/doc/current/service_container/import.html
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
default_api_ir_base_url: 'https://s.api.ir'
# Optional ISR webhook for the public site. Without defaults, a deploy that
# does not define these variables makes BlogCacheInvalidator un-instantiable,
# which takes the WHOLE blog module down with HTTP 500 — reads included.
# BlogCacheInvalidator is fail-open on an empty string: no webhook, no cache
# invalidation, everything else keeps working.
env(REVALIDATE_WEBHOOK_URL): ''
env(REVALIDATE_WEBHOOK_SECRET): ''
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# Tag every room-deletion reason so RoomService can iterate them without knowing
# who they are. Tasks 02 (active resources) and 07 (future appointments) each add
# one implementation and RoomService itself stays untouched.
_instanceof:
App\Branch\Service\RoomDeletionGuardInterface:
tags: ['app.room_deletion_guard']
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
App\Category\Command\SeedCategoriesCommand:
arguments:
$projectDir: '%kernel.project_dir%'
App\Shared\Command\SeedDemoDataCommand:
arguments:
$environment: '%kernel.environment%'
App\Shared\Service\FileUploadService:
arguments:
$projectDir: '%kernel.project_dir%'
App\Doctor\Controller\DoctorController:
arguments:
$projectDir: '%kernel.project_dir%'
App\Clinic\Controller\ClinicController:
arguments:
$projectDir: '%kernel.project_dir%'
App\Settlement\Controller\SettlementController:
arguments:
$projectDir: '%kernel.project_dir%'
App\UserProfile\Controller\UserProfileController:
arguments:
$projectDir: '%kernel.project_dir%'
App\Shared\Service\ApiIrService:
arguments:
$baseUrl: '%env(default:default_api_ir_base_url:API_IR_BASE_URL)%'
$token: '%env(default::API_IR_TOKEN)%'
App\Shared\Captcha\AltchaService:
arguments:
$hmacKey: '%env(ALTCHA_HMAC_KEY)%'
$envEnabled: '%env(bool:ALTCHA_ENABLED)%'
$maxNumber: '%env(int:ALTCHA_MAX_NUMBER)%'
$expireSeconds: '%env(int:ALTCHA_EXPIRE_SECONDS)%'
$altchaPool: '@altcha.pool'
App\Shared\Controller\HealthController:
arguments:
$healthPool: '@health.pool'
# Persist warning+ logs to the app_log table while keeping stderr output.
App\Shared\Logging\DbLogger:
decorates: 'logger'
arguments:
$inner: '@.inner'
$conn: '@doctrine.dbal.default_connection'
App\Auth\Service\OtpService:
arguments:
$otpTtl: '%env(int:OTP_TTL)%'
$appEnv: '%kernel.environment%'
App\Auth\Service\TokenService:
arguments:
$refreshTokenTtl: '%env(int:REFRESH_TOKEN_TTL)%'
App\Auth\Security\PasswordAuthenticator:
arguments:
$refreshTokenTtl: '%env(int:REFRESH_TOKEN_TTL)%'
$crawlerServiceToken: '%env(default::CRAWLER_SERVICE_TOKEN)%'
$loginLimiter: '@limiter.login'
App\Auth\Controller\AuthController:
arguments:
$sendCodeLimiter: '@limiter.send_code'
$verifyCodeLimiter: '@limiter.verify_code'
$tokenIssueLimiter: '@limiter.token_issue'
$passwordResetLimiter: '@limiter.password_reset'
App\Payment\Gateway\MellatGateway:
arguments:
$terminalId: '%env(default::MELLAT_TERMINAL_ID)%'
$username: '%env(default::MELLAT_USERNAME)%'
$password: '%env(default::MELLAT_PASSWORD)%'
App\Payment\Gateway\SepGateway:
arguments:
$terminalId: '%env(default::SEP_TERMINAL_ID)%'
App\Payment\Controller\PaymentController:
arguments:
$appBaseUrl: '%env(APP_BASE_URL)%'
$allowedFrontendHosts: '%env(ALLOWED_FRONTEND_HOSTS)%'
App\Payment\Service\PaymentManager:
arguments:
$appBaseUrl: '%env(APP_BASE_URL)%'
App\Sms\Provider\KavehNegarProvider:
arguments:
$apiKey: '%env(default::KAVENEGAR_API_KEY)%'
App\Blog\Controller\BlogController:
arguments:
$projectDir: '%kernel.project_dir%'
App\Blog\Service\BlogCacheInvalidator:
arguments:
$webhookUrl: '%env(REVALIDATE_WEBHOOK_URL)%'
$webhookSecret: '%env(REVALIDATE_WEBHOOK_SECRET)%'
App\Insurance\Controller\InsuranceController:
arguments:
$projectDir: '%kernel.project_dir%'
App\ClinicInvitation\Service\ClinicInvitationService:
arguments:
$appUrl: '%env(APP_BASE_URL)%'
App\ClinicInvitation\Controller\ClinicInvitationWebController:
arguments:
$appUrl: '%env(APP_BASE_URL)%'
App\Secretary\Controller\SecretaryController:
arguments:
$appUrl: '%env(APP_BASE_URL)%'
App\Secretary\Service\SecretaryService:
arguments:
$appUrl: '%env(APP_BASE_URL)%'
App\Auth\Controller\PreRegistrationController:
arguments:
$appUrl: '%env(APP_BASE_URL)%'
App\Sms\Controller\SmsWalletController:
arguments:
$appBaseUrl: '%env(APP_BASE_URL)%'