# پیاده‌سازی فاز ۲ — Backend تسک‌به‌تسک ## زمینه تسک‌های فاز ۲ در `docs/phase2_taskes/` طراحی شده‌اند (task-10 تا task-16). این پرامپت فقط **Backend** را پیاده‌سازی می‌کند — Entity، Migration، Repository، Service، Controller — و بعد از هر تسک، فایل داک مربوطه در `docs/api/` ایجاد یا به‌روز می‌کند. ## ترتیب اجباری تسک‌ها (وابستگی) ``` task-10 (Staff) ← پیش‌نیاز task-13 و task-15 ↓ task-11 (Subscription) ← پیش‌نیاز gate check در task-12، 13، 15 ↓ task-12 (Secretary) ← فقط SecretaryController تغییر می‌کند ↓ task-13 (Services) ← نیاز به Staff و Subscription ↓ task-14 (SMS Panel) ← مستقل، بعد از task-11 (نه الزامی) ↓ task-15 (Patient) ← نیاز به Staff + Subscription + Services ↓ task-16 (Dashboard) ← آخر — فقط query های موجود تغییر می‌کنند ``` ## قوانین عمومی پروژه ### Backend (Symfony 7) - همه controller‌ها از `App\Shared\Controller\BaseController` ارث می‌برند - پاسخ‌ها **فقط** با این متدها: - `$this->success($data, $httpStatus)` — single resource - `$this->paginated($items, $total, $page, $limit)` — لیست با صفحه‌بندی - `$this->error($code, $message, $httpStatus)` — خطا - `$this->validationError($violations)` — خطای validation (422) - تاریخ‌ها **همیشه** Unix timestamp (`int`) — نه DateTime، نه DateTimeImmutable - بعد از هر تغییر Entity: `doctrine:migrations:diff` و `doctrine:migrations:migrate` - Error code های جدید باید در `src/Shared/Constant/ErrorCodes.php` اضافه شوند ### ساختار Entity (الگو) ```php #[ORM\Entity(repositoryClass: XxxRepository::class)] #[ORM\Table(name: 'table_name')] class XxxEntity { #[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')] private ?int $id = null; #[ORM\Column(type: 'string', length: 36, unique: true)] private string $uuid; // polymorphic: #[ORM\Column(type: 'string', length: 10)] private string $entityType; // 'doctor' | 'clinic' #[ORM\Column(type: 'integer')] private int $entityId; #[ORM\Column(type: 'integer')] private int $createdAt; #[ORM\Column(type: 'integer')] private int $updatedAt; public function __construct() { $this->uuid = (string) \Symfony\Component\Uid\Uuid::v4(); $this->createdAt = time(); $this->updatedAt = time(); } } ``` ### توجه مهم — BaseController::success() double-nested ```php // اشتباه — double-nested data: return $this->success(['data' => $item->toArray()]); // نتیجه: { data: { data: {...} } } — frontend باید data?.data?.data بزند // درست: return $this->success($item->toArray()); // نتیجه: { data: {...} } // برای لیست paginated: array مستقیم (نه ['data' => ...]) return $this->paginated($items, $total, $page, $limit); ``` ### SecretaryController — وضعیت واقعی فعلی ```php // src/Secretary/Controller/SecretaryController.php (line 19) private const MAX_SECRETARIES = 1; // TODO موجود: link to subscription plan // متد create() در line 32 — دارد MAX_SECRETARIES را check می‌کند // اما هنوز SubscriptionService inject نشده // ERR_SECRETARY_001 موجود در ErrorCodes.php ``` ### Payment entity — وضعیت واقعی ```php // src/Payment/Entity/Payment.php public const TYPE_APPOINTMENT = 'appointment'; public const TYPE_SUBSCRIPTION = 'subscription'; // TYPE_SMS_WALLET هنوز وجود ندارد — باید اضافه شود ``` ### SmsService — signature واقعی ```php // src/Sms/Service/SmsService.php public function dispatchAsync(string $mobile, string $message, string $provider = 'kavenegar', ...): void public function sendNow(SendSmsMessage $msg): bool // entityType/entityId پارامتر ندارد — wallet deduct باید در caller انجام شود ``` ### AppointmentController::updateStatus — وضعیت واقعی ```php // src/Appointment/Controller/AppointmentController.php // بعد از $appointment->setStatus($newStatus) و flush: // باید PatientService::autoCreateOnAppointmentConfirm() اضافه شود ``` --- ## تسک‌ها --- ### TASK-10: مدیریت پرسنل (Staff) **مرجع:** `docs/phase2_taskes/task-10-staff/` #### فایل‌های جدید: ``` src/Staff/Entity/ClinicStaff.php src/Staff/Repository/ClinicStaffRepository.php src/Staff/Service/StaffService.php src/Staff/Controller/StaffController.php migrations/Version_YYYYMMDD_staff.php ``` #### Entity ClinicStaff: فیلدها: `id`, `uuid`, `entityType` (VARCHAR 10), `entityId` (INT), `fullName` (VARCHAR 200, required), `phone` (nullable), `jobTitle` (nullable), `address` (TEXT nullable), `nationalCode` (CHAR 10 nullable), `active` (bool=true), `createdAt` (int), `updatedAt` (int). Index: `(entity_type, entity_id, active)` #### Controller endpoints: ``` GET /api/v1/staff → لیست (entity از JWT) POST /api/v1/staff → ایجاد PATCH /api/v1/staff/{uuid} → ویرایش PATCH /api/v1/staff/{uuid}/toggle → toggle active ``` #### نکات: - entity_type و entity_id از JWT token claim (`db_key` و `db_uuid` → resolve به id) - toggle: فقط `active = !active` — هیچ حذفی نیست - ownership check: staff.entity_type/entityId == JWT claim - **بعد از پیاده‌سازی:** فایل `docs/api/staff.md` بساز --- ### TASK-11: پنل اشتراکی (Subscription Tiers) **مرجع:** `docs/phase2_taskes/task-11-subscription/` #### فایل‌های جدید: ``` src/Subscription/Entity/SubscriptionPlan.php src/Subscription/Entity/SubscriptionPeriod.php src/Subscription/Entity/ClinicSubscription.php src/Subscription/Repository/SubscriptionPlanRepository.php src/Subscription/Repository/SubscriptionPeriodRepository.php src/Subscription/Repository/ClinicSubscriptionRepository.php src/Subscription/Service/SubscriptionService.php src/Subscription/Controller/SubscriptionController.php migrations/Version_YYYYMMDD_subscription.php ``` #### SubscriptionPlan: `id`, `uuid`, `name` (VARCHAR 30: free/basic/professional), `level` (TINYINT), `maxSecretaries` (TINYINT), `features` (JSON: `{"patient_records":bool,"services":bool,"sms_panel":bool}`), `active` (bool), `createdAt`, `updatedAt`. #### SubscriptionPeriod: `id`, `uuid`, `plan` (FK→SubscriptionPlan), `label` (VARCHAR 50), `durationMonths` (TINYINT), `priceRials` (INT, 0 برای تریال), `isTrial` (bool=false), `active` (bool=true), `sortOrder` (TINYINT=0), `createdAt`, `updatedAt`. #### ClinicSubscription: `id`, `uuid`, `entityType`, `entityId`, `plan` (FK), `period` (FK), `payment` (FK nullable → Payment), `isTrial` (bool), `startsAt` (int), `expiresAt` (int nullable — NULL = بی‌نهایت)، `createdAt`. Index روی `(entity_type, entity_id, expires_at)`. #### SubscriptionService — متدهای الزامی: ```php public function getActiveSubscription(string $entityType, int $entityId): ?ClinicSubscription // query: WHERE entity_type=? AND entity_id=? AND (expires_at IS NULL OR expires_at > :now) ORDER BY id DESC LIMIT 1 public function hasFeature(string $entityType, int $entityId, string $feature): bool // false اگر اشتراک نداشت یا plan->features[$feature] = false public function getSecretaryLimit(string $entityType, int $entityId): int // اشتراک فعال → plan->maxSecretaries — اگر اشتراک نداشت: 1 (Free default) public function hasUsedTrial(string $entityType, int $entityId): bool public function activateTrial(string $entityType, int $entityId): ClinicSubscription // بررسی hasUsedTrial → throw اگر قبلاً استفاده شده // بررسی SiteConfig['trial_enabled'] != '0' // plan Basic پیدا کن → period is_trial=true پیدا کن // ClinicSubscription با payment=null بساز public function calculateExpiresAt(?int $currentExpiresAt, int $durationMonths): int // base = max($currentExpiresAt ?? 0, time()) // return base + durationMonths * 30 * 86400 ``` #### SubscriptionController endpoints: ``` GET /api/v1/subscription/plans → public — لیست با nested periods GET /api/v1/subscription/my → JWT — اشتراک فعال + used_trial + days_remaining POST /api/v1/subscription/trial → JWT — activateTrial() GET /api/v1/admin/subscription/plans → ROLE_ADMIN POST /api/v1/admin/subscription/plan → ROLE_ADMIN PATCH /api/v1/admin/subscription/plan/{uuid} → ROLE_ADMIN POST /api/v1/admin/subscription/period → ROLE_ADMIN PATCH /api/v1/admin/subscription/period/{uuid} → ROLE_ADMIN DELETE /api/v1/admin/subscription/period/{uuid} → ROLE_ADMIN (active=false) GET /api/v1/admin/subscription/report → ROLE_ADMIN ``` #### تغییر PaymentController: بعد از تأیید موفق callback نوع `TYPE_SUBSCRIPTION`: ```php // payment->type === Payment::TYPE_SUBSCRIPTION: // 1. از meta یا period_uuid پرداخت، SubscriptionPeriod پیدا کن // 2. subscriptionService->calculateExpiresAt(currentSub?->expiresAt, period->durationMonths) // 3. ClinicSubscription جدید بساز و persist کن ``` #### Seed data در migration: سه plan (free, basic, professional) + دوره‌های نمونه برای basic. #### Error codes جدید در ErrorCodes.php: ```php const ERR_SUBSCRIPTION_REQUIRED = 'ERR_SUBSCRIPTION_REQUIRED'; // 'این قابلیت نیاز به پنل Basic یا بالاتر دارد' const ERR_TRIAL_ALREADY_USED = 'ERR_TRIAL_ALREADY_USED'; // 'قبلاً از تریال استفاده کرده‌اید' const ERR_TRIAL_DISABLED = 'ERR_TRIAL_DISABLED'; // 'تریال در حال حاضر غیرفعال است' ``` **بعد از پیاده‌سازی:** فایل `docs/api/subscription.md` بساز. --- ### TASK-12: تکمیل منشی — محدودیت پنل **مرجع:** `docs/phase2_taskes/task-12-secretary-completion/` #### فایل‌های تغییرکننده (هیچ فایل جدیدی نیست): - `src/Secretary/Controller/SecretaryController.php` - `src/Secretary/Repository/DoctorSecretaryRepository.php` #### تغییر SecretaryController::create(): ```php // وضعیت فعلی (line 19): private const MAX_SECRETARIES = 1; // باید حذف شود و جایش با SubscriptionService جایگزین شود // inject SubscriptionService در constructor // در create() — جایگزین check فعلی: // $limit = $this->subscriptionService->getSecretaryLimit($entityType, $entityId); // $activeCount = $this->secretaryRepo->countActiveByDoctor($doctor); // متد موجود // if ($activeCount >= $limit) → error ERR_SECRETARY_001 ``` #### نکته مهم — متد موجود: `countActiveByDoctor(Doctor $doctor)` در `DoctorSecretaryRepository` **از قبل وجود دارد** — نیازی به ایجاد نیست. #### Error code موجود: `ERR_SECRETARY_001` در ErrorCodes.php موجود است — نیازی به اضافه کردن نیست. **بعد از پیاده‌سازی:** بخش «محدودیت پنل» را به `docs/api/secretary.md` اضافه کن. --- ### TASK-13: سرویس‌های کلینیک (Clinic Services) **مرجع:** `docs/phase2_taskes/task-13-clinic-services/` #### فایل‌های جدید: ``` src/ClinicService/Entity/ServiceSection.php src/ClinicService/Entity/ServiceItem.php src/ClinicService/Repository/ServiceSectionRepository.php src/ClinicService/Repository/ServiceItemRepository.php src/ClinicService/Controller/ClinicServiceController.php migrations/Version_YYYYMMDD_clinic_service.php ``` #### ServiceSection: `id`, `uuid`, `entityType`, `entityId`, `name` (VARCHAR 200), `active` (bool=true), `createdAt`, `updatedAt`. Index: `(entity_type, entity_id)`. #### ServiceItem: `id`, `uuid`, `section` (FK→ServiceSection ON DELETE CASCADE), `staff` (FK→ClinicStaff nullable ON DELETE SET NULL), `name` (VARCHAR 200), `priceRials` (INT=0), `active` (bool=true), `createdAt`, `updatedAt`. #### Controller endpoints: ``` GET /api/v1/service-sections → Basic+ gate POST /api/v1/service-section → Basic+ gate PATCH /api/v1/service-section/{uuid} → Basic+ gate DELETE /api/v1/service-section/{uuid} → Basic+ gate GET /api/v1/service-items/{sectionUuid} POST /api/v1/service-item → Basic+ gate PATCH /api/v1/service-item/{uuid} DELETE /api/v1/service-item/{uuid} ``` #### gate check helper: ```php private function assertServicesGate(): void { [$type, $id] = $this->resolveEntityFromJwt(); if (!$this->subscriptionService->hasFeature($type, $id, 'services')) { throw new \App\Shared\Exception\AppException(ErrorCodes::ERR_SUBSCRIPTION_REQUIRED, null, 403); } } ``` #### حذف ServiceItem با استفاده در پرونده: ```php // session_services.service_item_id FK ON DELETE RESTRICT // اگر در session_services باشد → DBAL ForeignKeyConstraintViolationException try { $this->em->remove($item); $this->em->flush(); } catch (\Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException) { return $this->error(ErrorCodes::ERR_SERVICE_ITEM_IN_USE, 'این سرویس در پرونده بیمار ثبت شده است', 409); } ``` #### Error codes جدید: ```php const ERR_SERVICE_ITEM_IN_USE = 'ERR_SERVICE_ITEM_IN_USE'; // 'این سرویس در پرونده بیمار ثبت شده است' ``` **بعد از پیاده‌سازی:** فایل `docs/api/clinic-services.md` بساز. --- ### TASK-14: پنل پیامکی (SMS Panel) **مرجع:** `docs/phase2_taskes/task-14-sms-panel/` #### فایل‌های جدید: ``` src/Sms/Entity/SmsWallet.php src/Sms/Entity/SmsWalletTransaction.php src/Sms/Entity/SmsSettings.php src/Sms/Repository/SmsWalletRepository.php src/Sms/Repository/SmsWalletTransactionRepository.php src/Sms/Repository/SmsSettingsRepository.php src/Sms/Service/SmsWalletService.php src/Sms/Controller/SmsWalletController.php migrations/Version_YYYYMMDD_sms_panel.php ``` #### فایل‌های تغییرکننده: - `src/Payment/Entity/Payment.php` — اضافه کردن `const TYPE_SMS_WALLET = 'sms_wallet'` - `src/Payment/Controller/PaymentController.php` — callback برای type=sms_wallet #### SmsWallet: `id`, `entityType`, `entityId`, `balanceRials` (INT=0), `createdAt`, `updatedAt`. UNIQUE INDEX روی `(entity_type, entity_id)`. #### SmsWalletTransaction: `id`, `uuid`, `wallet` (FK→SmsWallet ON DELETE CASCADE), `type` (VARCHAR 10: credit/debit), `amountRials` (INT), `description` (nullable), `payment` (FK→Payment nullable ON DELETE SET NULL), `createdAt`. #### SmsSettings: `id`, `entityType`, `entityId`, `reminderEnabled` (bool=false), `reminderHoursBefore` (TINYINT=2), `postVisitEnabled` (bool=false), `postVisitText` (TEXT nullable), `updatedAt`. UNIQUE INDEX روی `(entity_type, entity_id)`. #### SmsWalletService: ```php public function getOrCreate(string $entityType, int $entityId): SmsWallet public function charge(SmsWallet $wallet, int $amountRials, Payment $payment): void // ثبت credit tx public function deduct(SmsWallet $wallet, int $amountRials, string $description): bool // ثبت debit tx public function getBalance(string $entityType, int $entityId): int ``` #### SmsWalletController endpoints: ``` GET /api/v1/sms/wallet/balance → موجودی + sms_price_rials از SiteConfig + estimated_sms_count POST /api/v1/sms/wallet/charge → { gateway, amount_rials } → Payment TYPE_SMS_WALLET → redirect GET /api/v1/sms/wallet/logs → paginated SmsWalletTransactions GET /api/v1/sms/settings → SmsSettings (یا مقادیر پیش‌فرض) PATCH /api/v1/sms/settings → UPSERT SmsSettings GET /api/v1/admin/sms/wallet-report → ROLE_ADMIN ``` #### تغییر PaymentController callback برای sms_wallet: ```php if ($payment->getType() === Payment::TYPE_SMS_WALLET) { $wallet = $this->smsWalletService->getOrCreate($entityType, $entityId); $this->smsWalletService->charge($wallet, $payment->getAmountRials(), $payment); } ``` #### SiteConfig key: `sms_price_rials` — باید در `GET /api/v1/sms/wallet/balance` خوانده شود. **بعد از پیاده‌سازی:** بخش «SMS Wallet» و «SMS Settings» را به `docs/api/sms.md` اضافه کن. --- ### TASK-15: پرونده بیمار (Patient Records) **مرجع:** `docs/phase2_taskes/task-15-patient-records/` #### فایل‌های جدید: ``` src/Patient/Entity/PatientRecord.php src/Patient/Entity/PatientSession.php src/Patient/Entity/SessionService.php src/Patient/Repository/PatientRecordRepository.php src/Patient/Repository/PatientSessionRepository.php src/Patient/Service/PatientService.php src/Patient/Controller/PatientController.php migrations/Version_YYYYMMDD_patient.php ``` #### فایل تغییرکننده: - `src/Appointment/Controller/AppointmentController.php::updateStatus()` — بعد از persist #### PatientRecord: `id`, `uuid`, `entityType`, `entityId`, `user` (FK→User ON DELETE RESTRICT), `createdByType` (VARCHAR 15: doctor/secretary/system), `createdById` (INT), `createdAt`. UNIQUE: `(entity_type, entity_id, user_id)`. #### PatientSession: `id`, `uuid`, `record` (FK→PatientRecord CASCADE), `appointment` (FK nullable SET NULL), `insuranceBaseId` (INT nullable FK→categories), `insuranceSupplementaryId` (INT nullable FK→categories), `visitPriceRials` (INT=0), `baseInsuranceDiscountPercent` (DECIMAL 5,2=0), `supplementaryDiscountPercent` (DECIMAL 5,2=0), `servicesTotalRials` (INT=0), `finalPriceRials` (INT=0), `paymentMethod` (VARCHAR 15=pending), `notes` (TEXT nullable), `createdAt`, `updatedAt`. #### SessionService (entity): `id`, `uuid`, `session` (FK→PatientSession CASCADE), `serviceItem` (FK→ServiceItem ON DELETE RESTRICT), `staff` (FK→ClinicStaff nullable SET NULL), `priceRials` (INT — کپی در زمان ثبت)، `createdAt`. #### PatientService: ```php public function calculateFinalPrice( int $visitPrice, float $baseDiscount, float $suppDiscount, array $serviceItems ): array { $afterBase = $visitPrice * (1 - $baseDiscount / 100); $afterSupp = $afterBase * (1 - $suppDiscount / 100); $servicesTotal = array_sum(array_column($serviceItems, 'price_rials')); return [ 'services_total_rials' => $servicesTotal, 'final_price_rials' => (int) round($afterSupp) + $servicesTotal, ]; } public function autoCreateOnAppointmentConfirm(Appointment $appointment): void { // entityType/entityId از appointment doctor یا clinic // if !hasFeature('patient_records') → return (نه خطا) // findOrCreate PatientRecord با UNIQUE constraint // ایجاد PatientSession با appointment_id, visit_price=0, payment_method='pending' } ``` #### PatientController endpoints: ``` GET /api/v1/patients → paginated + ?search= — Basic+ gate POST /api/v1/patient → Basic+ gate — { user_uuid } GET /api/v1/patient/{uuid} → Basic+ gate GET /api/v1/patient/{uuid}/sessions → paginated POST /api/v1/patient/{uuid}/session → { appointment_uuid?, insurance_base_uuid?, visit_price_rials, base_insurance_discount_percent, supplementary_discount_percent, payment_method, notes?, services: [{service_item_uuid, staff_uuid?}] } PATCH /api/v1/session/{uuid} → ویرایش notes و payment_method ``` #### تغییر AppointmentController::updateStatus(): ```php // بعد از $this->em->flush()، اگر $newStatus === Appointment::STATUS_CONFIRMED: $this->patientService->autoCreateOnAppointmentConfirm($appointment); ``` #### Error codes جدید: ```php const ERR_PATIENT_NOT_FOUND = 'ERR_PATIENT_NOT_FOUND'; // 'پرونده بیمار یافت نشد' const ERR_SESSION_NOT_FOUND = 'ERR_SESSION_NOT_FOUND'; // 'مراجعه یافت نشد' ``` **بعد از پیاده‌سازی:** فایل `docs/api/patient.md` بساز. --- ### TASK-16: داشبورد هوشمند (Smart Dashboard) **مرجع:** `docs/phase2_taskes/task-16-smart-dashboard/` #### فایل‌های تغییرکننده (هیچ فایل جدیدی نیست): - `src/Dashboard/Controller/DashboardController.php` - `src/Admin/Controller/AdminApiController.php` #### تغییر DashboardController — اضافه کردن `from`/`to`: ```php // متدهای clinic() و doctor(): $from = (int) $request->query->get('from', strtotime('-30 days')); $to = (int) $request->query->get('to', time()); // فیلدهای جدید در response: 'sms_wallet_balance' => $this->smsWalletService->getBalance($entityType, $entityId), 'unique_patients_count' => $this->patientRecordRepo->countUnique($entityType, $entityId, $from, $to), 'revenue_period_rials' => $this->patientSessionRepo->sumRevenue($entityType, $entityId, $from, $to), ``` #### تغییر AdminApiController::dashboardCharts(): ```php $from = (int) $request->query->get('from', strtotime('-30 days')); $to = (int) $request->query->get('to', time()); // appointments_by_day: DQL با FLOOR(a.createdAt / 86400) * 86400 AS date, COUNT // revenue_by_day: از patient_sessions در بازه // subscription_sales_by_plan: از clinic_subscriptions در بازه (is_trial=0) ``` #### PatientRecordRepository متد جدید: ```php public function countUnique(string $entityType, int $entityId, int $from, int $to): int // COUNT(DISTINCT ps_session.user) با JOIN patient_sessions روی بازه ``` #### PatientSessionRepository متد جدید: ```php public function sumRevenue(string $entityType, int $entityId, int $from, int $to): int // SUM(final_price_rials) با JOIN patient_records ``` **بعد از پیاده‌سازی:** بخش «فیلتر زمانی» و «فیلدهای جدید» را به `docs/api/dashboard.md` اضافه کن. --- ## نکات مهم برای همه تسک‌ها ### resolveEntityFromJwt — helper مشترک هر controller باید entity_type و entity_id کاربر جاری را از JWT بخواند: ```php // از $this->getUser() (که User entity است) → db_key و db_uuid // db_key = 'doctor' یا 'clinic' // db_uuid → resolve به id از repository مربوطه // این منطق باید در BaseController یا یک trait مشترک باشد // بررسی کن آیا getUser()->getDbKey() / getDbUuid() موجود است ``` ### migration بعد از هر تسک: ```bash ddev exec php bin/console doctrine:migrations:diff --no-interaction ddev exec php bin/console doctrine:migrations:migrate --no-interaction ddev exec php bin/console cache:clear ``` ### بررسی route بعد از هر تسک: ```bash ddev exec php bin/console debug:router | grep "api/v1/staff\|subscription\|service-section\|sms/wallet\|patient" ``` ### ترتیب مستندسازی (docs/api/): | تسک | فایل | |-----|------| | task-10 | `docs/api/staff.md` ← **ایجاد جدید** | | task-11 | `docs/api/subscription.md` ← **ایجاد جدید** | | task-12 | `docs/api/secretary.md` ← **اضافه کردن بخش** | | task-13 | `docs/api/clinic-services.md` ← **ایجاد جدید** | | task-14 | `docs/api/sms.md` ← **اضافه کردن بخش** | | task-15 | `docs/api/patient.md` ← **ایجاد جدید** | | task-16 | `docs/api/dashboard.md` ← **اضافه کردن بخش** | هر فایل doc باید داشته باشد: method/path، permission، request body، response JSON، error codes.