Add comprehensive project documentation for ClinicPro in CLAUDE.md and README.md
- Introduced CLAUDE.md for internal guidance on project structure, commands, and architecture. - Created README.md with detailed project overview, technology stack, directory structure, setup instructions, API endpoints, authentication flow, and external services.
This commit is contained in:
@@ -62,3 +62,8 @@ KAVENEGAR_API_KEY=test_key
|
|||||||
KAVENEGAR_SENDER=1000596446
|
KAVENEGAR_SENDER=1000596446
|
||||||
RANGINEH_API_KEY=test_key
|
RANGINEH_API_KEY=test_key
|
||||||
RANGINEH_SENDER=3000
|
RANGINEH_SENDER=3000
|
||||||
|
|
||||||
|
###> API Docs ###
|
||||||
|
API_DOC_USERNAME=admin
|
||||||
|
API_DOC_PASSWORD='$2y$10$aBQlhiUjSK65c6Ly/daJ9OXSvu/NzYJ/bpVi4stpnC7vJY40SbEKG'
|
||||||
|
###< API Docs ###
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**ClinicPro** — a clinic management and appointment booking platform migrated from Drupal to Symfony 7. It consists of a Symfony REST API backend and a React 19 admin SPA bundled inside Symfony via Webpack Encore.
|
||||||
|
|
||||||
|
- **Local URL:** `https://clinic-pro.ddev.site`
|
||||||
|
- **Admin panel:** `https://clinic-pro.ddev.site/admin`
|
||||||
|
- **Swagger UI:** `https://clinic-pro.ddev.site/api/doc`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
All commands run inside ddev: prefix with `ddev exec` unless noted.
|
||||||
|
|
||||||
|
### Backend (PHP/Symfony)
|
||||||
|
```bash
|
||||||
|
ddev exec php bin/console cache:clear
|
||||||
|
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
ddev exec php bin/console doctrine:migrations:diff --no-interaction # generate migration after entity change
|
||||||
|
ddev exec php bin/console debug:router | grep api
|
||||||
|
ddev exec php bin/console messenger:consume async # start queue worker
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
ddev exec php bin/phpunit
|
||||||
|
ddev exec php bin/phpunit tests/SomeTest.php # single test file
|
||||||
|
|
||||||
|
# Static analysis
|
||||||
|
ddev exec php vendor/bin/phpstan analyse
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend (React/TypeScript)
|
||||||
|
```bash
|
||||||
|
ddev exec yarn dev # one-off dev build (use this to check for errors)
|
||||||
|
ddev exec yarn watch # watch mode
|
||||||
|
ddev exec yarn build # production build
|
||||||
|
|
||||||
|
# Type check only (faster)
|
||||||
|
ddev exec npx tsc --noEmit --project tsconfig.json
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** The CSS build has a known `lightningcss.linux-arm64-gnu.node` native module error inside ddev — this is pre-existing and does not block JS/TS compilation. TypeScript errors only appear in TSC output.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Backend — `src/`
|
||||||
|
|
||||||
|
Domain-driven structure; each domain is its own namespace under `App\<Domain>\`:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
Admin/Controller/AdminApiController.php # all admin-only list/stats endpoints
|
||||||
|
Appointment/ Doctor/ Clinic/
|
||||||
|
Auth/ Payment/ Rating/
|
||||||
|
Blog/ Representation/ Secretary/
|
||||||
|
Category/ Settlement/ Sms/
|
||||||
|
Shared/Controller/BaseController.php # all controllers extend this
|
||||||
|
Shared/Constant/ErrorCodes.php
|
||||||
|
```
|
||||||
|
|
||||||
|
**Every controller extends `BaseController`** which provides three response helpers:
|
||||||
|
|
||||||
|
| Method | Shape | When to use |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| `$this->success($data)` | `{ success, data: $data }` | Single resource / action |
|
||||||
|
| `$this->paginated($items, $total, $page, $limit)` | `{ success, data: $items[], meta: { totalRecords, totalPages, currentPage } }` | Admin list endpoints |
|
||||||
|
| `$this->error($code, $message, $status)` | `{ success:false, errors:[{code,message}] }` | All error responses |
|
||||||
|
|
||||||
|
**Critical pitfall — double-nested responses:**
|
||||||
|
`$this->success(['data' => $rep->toArray()])` produces `{ data: { data: {...} } }`, so the frontend must extract with `data?.data?.data`. The `paginated()` helper does NOT nest — it returns `data` as a flat array.
|
||||||
|
|
||||||
|
### Frontend — `assets/admin/`
|
||||||
|
|
||||||
|
Single-page app mounted at `/admin/*`:
|
||||||
|
|
||||||
|
```
|
||||||
|
assets/admin/
|
||||||
|
App.tsx # React Router routes
|
||||||
|
pages/ # one file per page
|
||||||
|
components/ui/ # DataTable, Modal, ConfirmDialog, PageHeader, StatusBadge, Pagination
|
||||||
|
lib/api.ts # fetch wrapper (reads JWT from localStorage key: clinicpro-auth)
|
||||||
|
lib/utils.ts # formatRial, formatNumber, formatDate, formatDateTime
|
||||||
|
types/index.ts # all TypeScript interfaces
|
||||||
|
stores/authStore.ts # Zustand auth store (persisted to localStorage)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Data fetching pattern:** TanStack Query v5 (`useQuery` / `useMutation`). Query keys use `['resource-name', page, filters]`.
|
||||||
|
|
||||||
|
**API response types in `lib/api.ts`:**
|
||||||
|
- `ApiResponse<T>` — for single-resource responses: extract with `data?.data`
|
||||||
|
- `PaginatedResponse<T>` — for admin lists: items at `data?.data`, total at `data?.meta?.totalRecords`
|
||||||
|
|
||||||
|
**Forms:** React Hook Form + Zod resolver. Schema defined with `z.object()`, type inferred with `z.infer<typeof schema>`.
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
|
||||||
|
- JWT stored in Zustand store → `localStorage['clinicpro-auth']` → `state.token`
|
||||||
|
- `api.ts` reads it automatically for every request
|
||||||
|
- Admin routes require `ROLE_ADMIN`. `#[IsGranted('ROLE_ADMIN')]` on controller class or method.
|
||||||
|
- Public endpoints listed in `config/packages/security.yaml` under `public_endpoints` firewall pattern
|
||||||
|
|
||||||
|
### Category / Bundle system
|
||||||
|
|
||||||
|
Categories are polymorphic via a `bundle` string field. Used for: `state`, `city`, `specially_doctor`, `doctor_services`, `insurance_type`, `supplementary_insurance`, `tag`.
|
||||||
|
|
||||||
|
City IDs (integer FK to `categories.id` where `bundle='city'`) are stored on entities like `Representation.cityId`. To get the city name, LEFT JOIN the categories table in DQL.
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
- MariaDB 11.8 via ddev
|
||||||
|
- Doctrine ORM with integer Unix timestamps (`createdAt`, `updatedAt`) — **not** DateTime objects
|
||||||
|
- All admin list queries in `AdminApiController` use DQL array hydration (`.getArrayResult()`) to avoid triggering non-existent getter errors on entities
|
||||||
|
- Migrations in `migrations/` — always run `doctrine:migrations:diff` after entity changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Patterns
|
||||||
|
|
||||||
|
**Adding a new admin list endpoint (backend):**
|
||||||
|
1. Add method to `src/Admin/Controller/AdminApiController.php`
|
||||||
|
2. Use `$this->em->createQueryBuilder()` with `->getArrayResult()` (never use entity getters in admin list queries)
|
||||||
|
3. Return `$this->paginated($items, $total, $page, $limit)`
|
||||||
|
|
||||||
|
**Adding a new admin page (frontend):**
|
||||||
|
1. Create `assets/admin/pages/XxxPage.tsx`
|
||||||
|
2. Use `PaginatedResponse<YourType>` with `useQuery`
|
||||||
|
3. Extract: `data?.data` for items, `data?.meta?.totalRecords` for total
|
||||||
|
4. Add route in `App.tsx`
|
||||||
|
5. Add UI components: `<DataTable>`, `<Pagination>`, `<Modal>`, `<ConfirmDialog>`
|
||||||
|
|
||||||
|
**Category API endpoint pattern:** `GET /api/v1/categorys/{bundle}` (note: typo `categorys` is intentional — existing route). Response is double-nested: extract array with `data?.data?.data ?? []`.
|
||||||
+172
-122
@@ -1,6 +1,6 @@
|
|||||||
# ClinicPro — مستندات کامل پروژه
|
# ClinicPro — مستندات کامل پروژه
|
||||||
|
|
||||||
> **تاریخ آخرین بهروزرسانی:** ۱۴۰۵/۰۳/۱۹ | **نسخه Symfony:** 7.4 | **PHP:** ≥ 8.2
|
> **تاریخ آخرین بهروزرسانی:** ۱۴۰۵/۰۳/۲۰ | **نسخه Symfony:** 7.4 | **PHP:** ≥ 8.2
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
- ارسال پیامک (KaveNegar / Rangineh)
|
- ارسال پیامک (KaveNegar / Rangineh)
|
||||||
- امتیاز و نظرات پزشکان
|
- امتیاز و نظرات پزشکان
|
||||||
- بلاگ
|
- بلاگ
|
||||||
|
- مستندات API تعاملی (Swagger UI) — محافظتشده با HTTP Basic Auth
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -53,8 +54,8 @@
|
|||||||
| Auth | JWT (lexik/jwt-authentication-bundle) |
|
| Auth | JWT (lexik/jwt-authentication-bundle) |
|
||||||
| Queue | Symfony Messenger |
|
| Queue | Symfony Messenger |
|
||||||
| Cache/Session | Redis |
|
| Cache/Session | Redis |
|
||||||
| Database | MySQL |
|
| Database | MariaDB 11.8 |
|
||||||
| API Docs | NelmioApiDocBundle (Swagger) |
|
| API Docs | NelmioApiDocBundle v5 + swagger-php v6 |
|
||||||
| Rate Limiting | Symfony Rate Limiter |
|
| Rate Limiting | Symfony Rate Limiter |
|
||||||
|
|
||||||
### Frontend (Admin)
|
### Frontend (Admin)
|
||||||
@@ -87,9 +88,7 @@ clinic-pro-symfony/
|
|||||||
│ │ │ ├── AdminLayout.tsx
|
│ │ │ ├── AdminLayout.tsx
|
||||||
│ │ │ ├── Sidebar.tsx
|
│ │ │ ├── Sidebar.tsx
|
||||||
│ │ │ └── Topbar.tsx
|
│ │ │ └── Topbar.tsx
|
||||||
│ │ ├── pages/
|
│ │ ├── pages/ ← ۲۲ صفحه پیادهسازیشده
|
||||||
│ │ │ ├── LoginPage.tsx
|
|
||||||
│ │ │ └── DashboardPage.tsx
|
|
||||||
│ │ ├── stores/
|
│ │ ├── stores/
|
||||||
│ │ │ ├── authStore.ts ← Zustand JWT store
|
│ │ │ ├── authStore.ts ← Zustand JWT store
|
||||||
│ │ │ └── uiStore.ts ← Sidebar state
|
│ │ │ └── uiStore.ts ← Sidebar state
|
||||||
@@ -105,20 +104,22 @@ clinic-pro-symfony/
|
|||||||
│ │ ├── doctrine.yaml
|
│ │ ├── doctrine.yaml
|
||||||
│ │ ├── lexik_jwt_authentication.yaml
|
│ │ ├── lexik_jwt_authentication.yaml
|
||||||
│ │ ├── messenger.yaml
|
│ │ ├── messenger.yaml
|
||||||
|
│ │ ├── nelmio_api_doc.yaml ← Swagger config
|
||||||
│ │ ├── rate_limiter.yaml
|
│ │ ├── rate_limiter.yaml
|
||||||
│ │ └── webpack_encore.yaml
|
│ │ └── webpack_encore.yaml
|
||||||
│ └── routes/
|
│ └── routes/
|
||||||
│ ├── security.yaml
|
│ ├── security.yaml
|
||||||
│ └── nelmio_api_doc.yaml
|
│ └── nelmio_api_doc.yaml
|
||||||
│
|
│
|
||||||
├── migrations/ ← 16 Doctrine migrations
|
├── migrations/ ← 17 Doctrine migrations
|
||||||
│
|
│
|
||||||
├── public/
|
├── public/
|
||||||
│ ├── index.php
|
│ ├── index.php
|
||||||
│ └── build/ ← Webpack output
|
│ ├── build/ ← Webpack output
|
||||||
|
│ └── bundles/nelmioapidoc/ ← Swagger UI assets (local)
|
||||||
│
|
│
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── Admin/Controller/ ← SPA catch-all
|
│ ├── Admin/Controller/ ← AdminApiController + SPA catch-all
|
||||||
│ ├── Appointment/ ← نوبتدهی
|
│ ├── Appointment/ ← نوبتدهی
|
||||||
│ ├── Auth/ ← احراز هویت
|
│ ├── Auth/ ← احراز هویت
|
||||||
│ ├── Blog/ ← بلاگ
|
│ ├── Blog/ ← بلاگ
|
||||||
@@ -183,39 +184,49 @@ npm run watch
|
|||||||
```
|
```
|
||||||
|
|
||||||
### دسترسی
|
### دسترسی
|
||||||
| سرویس | آدرس |
|
|
||||||
|-------|------|
|
| سرویس | آدرس | توضیح |
|
||||||
| Admin Panel | https://clinic-pro.ddev.site/admin |
|
|-------|------|-------|
|
||||||
| API | https://clinic-pro.ddev.site/api/v1 |
|
| Admin Panel | https://clinic-pro.ddev.site/admin | JWT auth |
|
||||||
| Swagger UI | https://clinic-pro.ddev.site/api/doc |
|
| API | https://clinic-pro.ddev.site/api/v1 | REST API |
|
||||||
| Health Check | https://clinic-pro.ddev.site/health |
|
| Swagger UI | https://clinic-pro.ddev.site/api/doc | HTTP Basic Auth |
|
||||||
|
| Health Check | https://clinic-pro.ddev.site/health | عمومی |
|
||||||
|
|
||||||
### اطلاعات ادمین پیشفرض
|
### اطلاعات ادمین پیشفرض
|
||||||
|
|
||||||
| فیلد | مقدار |
|
| فیلد | مقدار |
|
||||||
|------|-------|
|
|------|-------|
|
||||||
| موبایل | `09120671713` |
|
| موبایل | `09120671713` |
|
||||||
| رمز عبور | `admin1234` |
|
| رمز عبور | `admin1234` |
|
||||||
| نقش | `ROLE_ADMIN` |
|
| نقش | `ROLE_ADMIN` |
|
||||||
|
|
||||||
|
### اطلاعات ورود به Swagger UI
|
||||||
|
|
||||||
|
| فیلد | مقدار پیشفرض |
|
||||||
|
|------|--------------|
|
||||||
|
| Username | `admin` |
|
||||||
|
| Password | `clinic-pro-docs` |
|
||||||
|
|
||||||
|
> برای تغییر رمز: `ddev exec php -r "echo password_hash('رمز-جدید', PASSWORD_BCRYPT) . PHP_EOL;"` — hash را در `.env` در `API_DOC_PASSWORD` قرار دهید.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ۵. متغیرهای محیطی
|
## ۵. متغیرهای محیطی
|
||||||
|
|
||||||
فایل `.env` — کلیدها (مقادیر در `.env.local` تنظیم میشوند):
|
فایل `.env` — کلیدها (مقادیر حساس در `.env.local` تنظیم میشوند):
|
||||||
|
|
||||||
```env
|
```env
|
||||||
APP_ENV=dev|prod
|
APP_ENV=dev|prod
|
||||||
APP_SECRET= # کلید امنیتی Symfony
|
APP_SECRET= # کلید امنیتی Symfony
|
||||||
APP_SHARE_DIR= # مسیر shared assets
|
|
||||||
DEFAULT_URI= # آدرس پایه سایت (مثلاً https://clinic-pro.ddev.site)
|
DEFAULT_URI= # آدرس پایه سایت (مثلاً https://clinic-pro.ddev.site)
|
||||||
|
|
||||||
DATABASE_URL= # DSN پایگاه داده MySQL
|
DATABASE_URL= # DSN پایگاه داده MariaDB
|
||||||
|
|
||||||
JWT_SECRET_KEY= # مسیر کلید خصوصی JWT
|
JWT_SECRET_KEY= # مسیر کلید خصوصی JWT
|
||||||
JWT_PUBLIC_KEY= # مسیر کلید عمومی JWT
|
JWT_PUBLIC_KEY= # مسیر کلید عمومی JWT
|
||||||
JWT_PASSPHRASE= # رمز کلید JWT
|
JWT_PASSPHRASE= # رمز کلید JWT
|
||||||
|
|
||||||
CORS_ALLOW_ORIGIN= # آدرسهای مجاز CORS
|
CORS_ALLOW_ORIGIN= # آدرسهای مجاز CORS (regex)
|
||||||
|
|
||||||
MESSENGER_TRANSPORT_DSN= # DSN صف پیام (Redis)
|
MESSENGER_TRANSPORT_DSN= # DSN صف پیام (Redis)
|
||||||
REDIS_URL= # آدرس Redis
|
REDIS_URL= # آدرس Redis
|
||||||
@@ -223,10 +234,9 @@ REDIS_URL= # آدرس Redis
|
|||||||
REFRESH_TOKEN_TTL=2592000 # عمر refresh token (ثانیه) = ۳۰ روز
|
REFRESH_TOKEN_TTL=2592000 # عمر refresh token (ثانیه) = ۳۰ روز
|
||||||
OTP_TTL=1200 # عمر کد OTP (ثانیه) = ۲۰ دقیقه
|
OTP_TTL=1200 # عمر کد OTP (ثانیه) = ۲۰ دقیقه
|
||||||
|
|
||||||
# SMS - KaveNegar
|
# SMS
|
||||||
KAVENEGAR_API_KEY=
|
KAVENEGAR_API_KEY=
|
||||||
KAVENEGAR_SENDER=
|
KAVENEGAR_SENDER=
|
||||||
# SMS - Rangineh
|
|
||||||
RANGINEH_API_KEY=
|
RANGINEH_API_KEY=
|
||||||
RANGINEH_SENDER=
|
RANGINEH_SENDER=
|
||||||
SMS_PROVIDER=kavenegar|rangineh # ارائهدهنده فعال
|
SMS_PROVIDER=kavenegar|rangineh # ارائهدهنده فعال
|
||||||
@@ -235,17 +245,17 @@ SMS_PROVIDER=kavenegar|rangineh # ارائهدهنده فعال
|
|||||||
MAX_FILE_SIZE_BYTES=5242880 # حداکثر حجم فایل (۵ مگابایت)
|
MAX_FILE_SIZE_BYTES=5242880 # حداکثر حجم فایل (۵ مگابایت)
|
||||||
UPLOAD_DIR= # مسیر ذخیره فایلها
|
UPLOAD_DIR= # مسیر ذخیره فایلها
|
||||||
|
|
||||||
|
# Payment
|
||||||
ALLOWED_FRONTEND_HOSTS= # هاستهای مجاز برای redirect پرداخت
|
ALLOWED_FRONTEND_HOSTS= # هاستهای مجاز برای redirect پرداخت
|
||||||
|
|
||||||
# Payment - Mellat Bank
|
|
||||||
MELLAT_TERMINAL_ID=
|
MELLAT_TERMINAL_ID=
|
||||||
MELLAT_USERNAME=
|
MELLAT_USERNAME=
|
||||||
MELLAT_PASSWORD=
|
MELLAT_PASSWORD=
|
||||||
|
|
||||||
# Payment - SEP (Saman)
|
|
||||||
SEP_TERMINAL_ID=
|
SEP_TERMINAL_ID=
|
||||||
|
|
||||||
APP_BASE_URL= # آدرس پایه برای callback پرداخت
|
APP_BASE_URL= # آدرس پایه برای callback پرداخت
|
||||||
|
|
||||||
|
# API Documentation (Swagger UI)
|
||||||
|
API_DOC_USERNAME=admin # نام کاربری ورود به /api/doc
|
||||||
|
API_DOC_PASSWORD= # bcrypt hash رمز عبور
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -280,6 +290,7 @@ APP_BASE_URL= # آدرس پایه برای callback پردا
|
|||||||
| `doctor_insurances` | `Insurance\Entity\DoctorInsurance` | بیمههای پزشک |
|
| `doctor_insurances` | `Insurance\Entity\DoctorInsurance` | بیمههای پزشک |
|
||||||
|
|
||||||
### جداول Join (ManyToMany)
|
### جداول Join (ManyToMany)
|
||||||
|
|
||||||
| جدول | رابطه |
|
| جدول | رابطه |
|
||||||
|------|-------|
|
|------|-------|
|
||||||
| `doctor_specialties` | Doctor ↔ Category (specialty) |
|
| `doctor_specialties` | Doctor ↔ Category (specialty) |
|
||||||
@@ -348,8 +359,8 @@ telephone VARCHAR(50) nullable
|
|||||||
is_24_7 BOOL default:false
|
is_24_7 BOOL default:false
|
||||||
working_days VARCHAR(255) nullable
|
working_days VARCHAR(255) nullable
|
||||||
latitude/longitude FLOAT nullable
|
latitude/longitude FLOAT nullable
|
||||||
city_id INT nullable → categories
|
city_id INT nullable → categories (bundle='city')
|
||||||
state_id INT nullable → categories
|
state_id INT nullable → categories (bundle='state')
|
||||||
representation_id INT nullable
|
representation_id INT nullable
|
||||||
images_clinic JSON nullable
|
images_clinic JSON nullable
|
||||||
clinic_logo JSON nullable
|
clinic_logo JSON nullable
|
||||||
@@ -357,6 +368,25 @@ clinic_logo JSON nullable
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Representation (representations)
|
||||||
|
|
||||||
|
```
|
||||||
|
id INT PK
|
||||||
|
uuid VARCHAR(36) UNIQUE
|
||||||
|
full_name VARCHAR(255)
|
||||||
|
mobile_number VARCHAR(20)
|
||||||
|
city_id INT nullable → categories (bundle='city') ← ID شهر (نه نام)
|
||||||
|
wallet_balance INT default:0
|
||||||
|
commission_rate FLOAT default:10.0
|
||||||
|
is_active BOOL default:true
|
||||||
|
created_at INT (unix timestamp)
|
||||||
|
updated_at INT (unix timestamp)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **توجه:** `city_id` یک FK عددی به جدول `categories` (bundle='city') است. نام شهر از طریق JOIN در API برگردانده میشود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Appointment (appointments)
|
### Appointment (appointments)
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -423,13 +453,13 @@ representation_id INT nullable
|
|||||||
|
|
||||||
**نوعهای bundle:**
|
**نوعهای bundle:**
|
||||||
```
|
```
|
||||||
state ← استانها
|
state ← استانها
|
||||||
city ← شهرها (parent_id = state)
|
city ← شهرها (parent_id = state.id)
|
||||||
specially_doctor ← تخصص پزشک
|
specially_doctor ← تخصص پزشک
|
||||||
doctor_services ← خدمات پزشک
|
doctor_services ← خدمات پزشک
|
||||||
insurance_type ← نوع بیمه پایه
|
insurance_type ← نوع بیمه پایه
|
||||||
supplementary_insurance ← بیمه تکمیلی
|
supplementary_insurance ← بیمه تکمیلی
|
||||||
tag ← تگ بلاگ
|
tag ← تگ بلاگ
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -469,6 +499,8 @@ active BOOL
|
|||||||
|
|
||||||
### Base URL: `https://clinic-pro.ddev.site`
|
### Base URL: `https://clinic-pro.ddev.site`
|
||||||
|
|
||||||
|
> مستندات کامل تعاملی: **https://clinic-pro.ddev.site/api/doc** (نیاز به HTTP Basic Auth)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🔐 Auth (`/api/v1/user/` & `/oauth/`)
|
### 🔐 Auth (`/api/v1/user/` & `/oauth/`)
|
||||||
@@ -479,11 +511,11 @@ active BOOL
|
|||||||
| POST | `/api/v1/user/send-code` | Public | ارسال کد OTP به موبایل |
|
| POST | `/api/v1/user/send-code` | Public | ارسال کد OTP به موبایل |
|
||||||
| POST | `/api/v1/user/verify-code` | Public | تأیید کد OTP |
|
| POST | `/api/v1/user/verify-code` | Public | تأیید کد OTP |
|
||||||
| POST | `/api/v1/user/register` | Public | ثبتنام کاربر جدید |
|
| POST | `/api/v1/user/register` | Public | ثبتنام کاربر جدید |
|
||||||
| POST | `/oauth/token` | Public | دریافت JWT با موبایل (mobile grant) |
|
| POST | `/oauth/token` | Public | دریافت JWT (mobile grant) |
|
||||||
| POST | `/oauth/token/refresh` | Public | تمدید JWT با refresh token |
|
| POST | `/oauth/token/refresh` | Public | تمدید JWT با refresh token |
|
||||||
| GET | `/oauth/userinfo` | ✅ | اطلاعات کاربر جاری |
|
| GET | `/oauth/userinfo` | ✅ | اطلاعات کاربر جاری |
|
||||||
| POST | `/oauth/logout` | ✅ | خروج و ابطال refresh token |
|
| POST | `/oauth/logout` | ✅ | خروج و ابطال refresh token |
|
||||||
| GET | `/session/token` | Public | دریافت CSRF session token |
|
| GET | `/session/token` | Public | CSRF session token |
|
||||||
|
|
||||||
**درخواست Login:**
|
**درخواست Login:**
|
||||||
```json
|
```json
|
||||||
@@ -503,15 +535,6 @@ POST /api/v1/user/login
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 👥 Users
|
|
||||||
|
|
||||||
| Method | Path | Auth | توضیح |
|
|
||||||
|--------|------|------|-------|
|
|
||||||
| GET | `/oauth/userinfo` | ✅ | پروفایل کاربر جاری |
|
|
||||||
| DELETE | `/api/v1/user/{id}` | ADMIN | حذف کاربر |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🩺 Doctors
|
### 🩺 Doctors
|
||||||
|
|
||||||
| Method | Path | Auth | توضیح |
|
| Method | Path | Auth | توضیح |
|
||||||
@@ -569,7 +592,7 @@ POST /api/v1/user/login
|
|||||||
| Method | Path | Auth | توضیح |
|
| Method | Path | Auth | توضیح |
|
||||||
|--------|------|------|-------|
|
|--------|------|------|-------|
|
||||||
| POST | `/api/v1/payment/appointment` | ✅ | شروع پرداخت نوبت |
|
| POST | `/api/v1/payment/appointment` | ✅ | شروع پرداخت نوبت |
|
||||||
| GET/POST | `/api/v1/payment/callback/{gateway}` | Public (IP) | callback درگاه |
|
| GET/POST | `/api/v1/payment/callback/{gateway}` | Public | callback درگاه |
|
||||||
| GET | `/api/v1/payment/{uuid}` | ✅ | وضعیت پرداخت |
|
| GET | `/api/v1/payment/{uuid}` | ✅ | وضعیت پرداخت |
|
||||||
| POST | `/api/v1/subscription-payment` | ✅ | پرداخت اشتراک |
|
| POST | `/api/v1/subscription-payment` | ✅ | پرداخت اشتراک |
|
||||||
|
|
||||||
@@ -601,9 +624,6 @@ POST /api/v1/user/login
|
|||||||
| GET | `/api/v1/comments/{doctorUuid}` | Public | نظرات تأییدشده |
|
| GET | `/api/v1/comments/{doctorUuid}` | Public | نظرات تأییدشده |
|
||||||
| DELETE | `/api/v1/comment/{uuid}` | ✅ | حذف نظر |
|
| DELETE | `/api/v1/comment/{uuid}` | ✅ | حذف نظر |
|
||||||
| POST | `/api/v1/like/{commentUuid}` | ✅ | لایک/آنلایک نظر |
|
| POST | `/api/v1/like/{commentUuid}` | ✅ | لایک/آنلایک نظر |
|
||||||
| GET | `/api/v1/admin/comments/pending` | ADMIN | نظرات در انتظار |
|
|
||||||
| POST | `/api/v1/admin/comment/{uuid}/approve` | ADMIN | تأیید نظر |
|
|
||||||
| POST | `/api/v1/admin/comment/{uuid}/reject` | ADMIN | رد نظر |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -620,7 +640,7 @@ POST /api/v1/user/login
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🔐 Secretaries (منشیها)
|
### 🔑 Secretaries (منشیها)
|
||||||
|
|
||||||
| Method | Path | Auth | توضیح |
|
| Method | Path | Auth | توضیح |
|
||||||
|--------|------|------|-------|
|
|--------|------|------|-------|
|
||||||
@@ -651,17 +671,13 @@ POST /api/v1/user/login
|
|||||||
|
|
||||||
| Method | Path | Auth | توضیح |
|
| Method | Path | Auth | توضیح |
|
||||||
|--------|------|------|-------|
|
|--------|------|------|-------|
|
||||||
| GET | `/api/v1/categorys/state` | Public | لیست استانها |
|
| GET | `/api/v1/categorys/{bundle}` | Public | لیست دستهبندی (`?state_id=X`) |
|
||||||
| GET | `/api/v1/categorys/city` | Public | لیست شهرها (`?state_id=X`) |
|
|
||||||
| GET | `/api/v1/categorys/specially_doctor` | Public | تخصصهای پزشکی |
|
|
||||||
| GET | `/api/v1/categorys/doctor_services` | Public | خدمات پزشکی |
|
|
||||||
| GET | `/api/v1/categorys/insurance_type` | Public | انواع بیمه پایه |
|
|
||||||
| GET | `/api/v1/categorys/supplementary_insurance` | Public | بیمه تکمیلی |
|
|
||||||
| GET | `/api/v1/categorys/tag` | Public | تگهای بلاگ |
|
|
||||||
| POST | `/api/v1/category` | ADMIN | ایجاد دستهبندی |
|
| POST | `/api/v1/category` | ADMIN | ایجاد دستهبندی |
|
||||||
| PATCH | `/api/v1/category/{id}` | ADMIN | ویرایش |
|
| PATCH | `/api/v1/category/{id}` | ADMIN | ویرایش |
|
||||||
| DELETE | `/api/v1/category/{id}` | ADMIN | حذف |
|
| DELETE | `/api/v1/category/{id}` | ADMIN | حذف |
|
||||||
|
|
||||||
|
**مقادیر `{bundle}`:** `state` | `city` | `specially_doctor` | `doctor_services` | `insurance_type` | `supplementary_insurance` | `tag`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 📝 Blog
|
### 📝 Blog
|
||||||
@@ -677,6 +693,27 @@ POST /api/v1/user/login
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 🖥 Admin API (`/api/v1/admin/`)
|
||||||
|
|
||||||
|
همه endpointهای ادمین نیاز به `ROLE_ADMIN` دارند:
|
||||||
|
|
||||||
|
| Method | Path | توضیح |
|
||||||
|
|--------|------|-------|
|
||||||
|
| GET | `/api/v1/admin/users` | لیست کاربران (`?search=`) |
|
||||||
|
| GET | `/api/v1/admin/appointments` | لیست نوبتها |
|
||||||
|
| GET | `/api/v1/admin/payments` | لیست پرداختها |
|
||||||
|
| GET | `/api/v1/admin/representations` | لیست نمایندگان (`?city_id=`) |
|
||||||
|
| GET | `/api/v1/admin/secretaries` | لیست منشیها |
|
||||||
|
| GET | `/api/v1/admin/rates` | لیست امتیازها |
|
||||||
|
| GET | `/api/v1/admin/comments` | لیست نظرات |
|
||||||
|
| GET | `/api/v1/admin/sms/logs` | لاگ پیامکها |
|
||||||
|
| GET | `/api/v1/admin/settlements` | لیست تسویهحسابها |
|
||||||
|
| GET | `/api/v1/admin/sms/sample-templates` | قالبهای نمونه پیامک |
|
||||||
|
| GET | `/api/v1/admin/dashboard/stats` | آمار کلی داشبورد |
|
||||||
|
| GET | `/api/v1/admin/dashboard/recent` | فعالیتهای اخیر |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### ❤️ Health
|
### ❤️ Health
|
||||||
|
|
||||||
| Method | Path | توضیح |
|
| Method | Path | توضیح |
|
||||||
@@ -698,18 +735,19 @@ POST /api/v1/user/login
|
|||||||
[Symfony] → Rate Limit check (IP)
|
[Symfony] → Rate Limit check (IP)
|
||||||
→ findByMobile()
|
→ findByMobile()
|
||||||
→ verify password_hash
|
→ verify password_hash
|
||||||
→ check isStaff() (ROLE_ADMIN or ROLE_DOCTOR or ROLE_CLINIC)
|
→ check isStaff()
|
||||||
|
|
||||||
↓ موفق
|
↓ موفق
|
||||||
|
|
||||||
[Response] → {
|
[Response] → {
|
||||||
access_token: "eyJ...", // ← JWT، عمر: 1 ساعت
|
access_token: "eyJ...", // JWT — عمر: ۱ ساعت
|
||||||
refresh_token: "8e75...", // ← در DB کش، عمر: 30 روز
|
refresh_token: "8e75...", // در DB کش — عمر: ۳۰ روز
|
||||||
expires_in: 3600
|
expires_in: 3600
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### JWT Token Payload
|
### JWT Token Payload
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"iat": 1781031215,
|
"iat": 1781031215,
|
||||||
@@ -720,26 +758,37 @@ POST /api/v1/user/login
|
|||||||
```
|
```
|
||||||
|
|
||||||
### استفاده از Token
|
### استفاده از Token
|
||||||
|
|
||||||
```
|
```
|
||||||
Authorization: Bearer eyJ...
|
Authorization: Bearer eyJ...
|
||||||
```
|
```
|
||||||
|
|
||||||
### Refresh Token
|
|
||||||
```
|
|
||||||
POST /oauth/token/refresh
|
|
||||||
{ "refresh_token": "8e75..." }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Firewalls
|
### Firewalls
|
||||||
|
|
||||||
```
|
```
|
||||||
dev → مسیرهای profiler/assets (no security)
|
dev → ^/(_profiler|_wdt|assets|build)/ — no security
|
||||||
health → /health (no security)
|
health → ^/health$ — no security
|
||||||
public_endpoints → مسیرهای عمومی API (no security)
|
api_doc → ^/api/doc — HTTP Basic Auth (InMemoryUser)
|
||||||
payment_callback → callback درگاه (no security)
|
public_endpoints → مسیرهای عمومی API — no security
|
||||||
api → ^/(api|oauth)/ — JWT authenticator
|
payment_callback → callback درگاه پرداخت — no security
|
||||||
|
api → ^/(api|oauth)/ — JWT authenticator
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### محافظت از Swagger UI
|
||||||
|
|
||||||
|
مسیر `/api/doc` از طریق یک firewall جداگانه با **HTTP Basic Authentication** محافظت میشود:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# config/packages/security.yaml
|
||||||
|
api_doc:
|
||||||
|
pattern: ^/api/doc
|
||||||
|
http_basic:
|
||||||
|
realm: "ClinicPro API Documentation"
|
||||||
|
provider: api_doc_provider
|
||||||
|
```
|
||||||
|
|
||||||
|
اطلاعات ورود از متغیرهای محیطی `API_DOC_USERNAME` و `API_DOC_PASSWORD` (bcrypt hash) خوانده میشود.
|
||||||
|
|
||||||
### نقشها و دسترسیها
|
### نقشها و دسترسیها
|
||||||
|
|
||||||
| نقش | دسترسی |
|
| نقش | دسترسی |
|
||||||
@@ -779,25 +828,33 @@ send_code: 3 درخواست در 5 دقیقه (per IP)
|
|||||||
[React Router] مسیریابی client-side
|
[React Router] مسیریابی client-side
|
||||||
```
|
```
|
||||||
|
|
||||||
### مسیرهای React
|
### مسیرهای React (پیادهسازیشده)
|
||||||
|
|
||||||
| مسیر | صفحه | Auth |
|
| مسیر | صفحه | توضیح |
|
||||||
|------|------|------|
|
|------|------|-------|
|
||||||
| `/admin/login` | LoginPage | عمومی |
|
| `/admin/login` | LoginPage | ورود با JWT |
|
||||||
| `/admin/dashboard` | DashboardPage | ✅ |
|
| `/admin/dashboard` | DashboardPage | آمار واقعی از API |
|
||||||
| `/admin/users` | *(در حال توسعه)* | ✅ |
|
| `/admin/users` | UsersPage | لیست کاربران |
|
||||||
| `/admin/doctors` | *(در حال توسعه)* | ✅ |
|
| `/admin/users/:uuid` | UserDetailPage | جزئیات کاربر |
|
||||||
| `/admin/clinics` | *(در حال توسعه)* | ✅ |
|
| `/admin/doctors` | DoctorsPage | لیست پزشکان |
|
||||||
| `/admin/appointments` | *(در حال توسعه)* | ✅ |
|
| `/admin/doctors/:uuid` | DoctorDetailPage | جزئیات پزشک |
|
||||||
| `/admin/payments` | *(در حال توسعه)* | ✅ |
|
| `/admin/clinics` | ClinicsPage | لیست کلینیکها |
|
||||||
| `/admin/settlements` | *(در حال توسعه)* | ✅ |
|
| `/admin/clinics/:uuid` | ClinicDetailPage | جزئیات کلینیک |
|
||||||
| `/admin/comments` | *(در حال توسعه)* | ✅ |
|
| `/admin/appointments` | AppointmentsPage | لیست نوبتها |
|
||||||
| `/admin/ratings` | *(در حال توسعه)* | ✅ |
|
| `/admin/appointments/:uuid` | AppointmentDetailPage | جزئیات نوبت |
|
||||||
| `/admin/sms` | *(در حال توسعه)* | ✅ |
|
| `/admin/payments` | PaymentsPage | لیست پرداختها |
|
||||||
| `/admin/categories` | *(در حال توسعه)* | ✅ |
|
| `/admin/payments/:uuid` | PaymentDetailPage | جزئیات پرداخت |
|
||||||
| `/admin/blogs` | *(در حال توسعه)* | ✅ |
|
| `/admin/settlements` | SettlementsPage | لیست تسویهحسابها |
|
||||||
| `/admin/representations` | *(در حال توسعه)* | ✅ |
|
| `/admin/representations` | RepresentationsPage | لیست نمایندگان (فیلتر شهر) |
|
||||||
| `/admin/secretaries` | *(در حال توسعه)* | ✅ |
|
| `/admin/representations/:uuid` | RepresentationDetailPage | جزئیات نماینده |
|
||||||
|
| `/admin/comments` | CommentsPage | مدیریت نظرات |
|
||||||
|
| `/admin/ratings` | RatingsPage | لیست امتیازها |
|
||||||
|
| `/admin/sms` | SmsPage | مدیریت پیامک |
|
||||||
|
| `/admin/categories` | CategoriesPage | مدیریت دستهبندیها |
|
||||||
|
| `/admin/blogs` | BlogsPage | لیست مقالات |
|
||||||
|
| `/admin/blogs/new` | BlogFormPage | ایجاد مقاله |
|
||||||
|
| `/admin/blogs/:uuid/edit` | BlogFormPage | ویرایش مقاله |
|
||||||
|
| `/admin/secretaries` | SecretariesPage | مدیریت منشیها |
|
||||||
|
|
||||||
### Auth Guard
|
### Auth Guard
|
||||||
|
|
||||||
@@ -838,12 +895,13 @@ direction: rtl;
|
|||||||
### دستورات npm
|
### دستورات npm
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev # build یکبار (dev)
|
ddev exec yarn dev # build یکبار (dev)
|
||||||
npm run watch # build + watch
|
ddev exec yarn watch # build + watch
|
||||||
npm run build # build production (minified + hashed)
|
ddev exec yarn build # build production (minified + hashed)
|
||||||
npm run dev-server # webpack dev server (HMR)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **توجه:** خطای `lightningcss.linux-arm64-gnu.node` در محیط ddev از پیش وجود دارد و JS/TS compilation را مسدود نمیکند.
|
||||||
|
|
||||||
### فایلهای خروجی
|
### فایلهای خروجی
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -870,54 +928,46 @@ public/build/
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### تنظیمات PostCSS (`postcss.config.js`)
|
|
||||||
|
|
||||||
```js
|
|
||||||
module.exports = {
|
|
||||||
plugins: { '@tailwindcss/postcss': {} }
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ۱۱. دستورات Console
|
## ۱۱. دستورات Console
|
||||||
|
|
||||||
### دستور موجود
|
### دستورات کاربردی
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# لغو خودکار نوبتهای منقضیشده
|
# لغو خودکار نوبتهای منقضیشده
|
||||||
ddev exec php bin/console app:cancel-expired-appointments
|
ddev exec php bin/console app:cancel-expired-appointments
|
||||||
|
|
||||||
# توضیح: نوبتهایی که slot_start آنها گذشته و هنوز pending هستند
|
|
||||||
# را به وضعیت auto_cancel_unpaid تغییر میدهد
|
|
||||||
# اجرا: از طریق cron job (مثلاً هر ۱۵ دقیقه)
|
|
||||||
```
|
|
||||||
|
|
||||||
### دستورات Symfony مفید
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# مشاهده همه routeها
|
# مشاهده همه routeها
|
||||||
ddev exec php bin/console debug:router
|
ddev exec php bin/console debug:router | grep api
|
||||||
|
|
||||||
# پاک کردن کش
|
# پاک کردن کش
|
||||||
ddev exec php bin/console cache:clear
|
ddev exec php bin/console cache:clear
|
||||||
|
|
||||||
# اجرای migrations
|
# اجرای migrations
|
||||||
ddev exec php bin/console doctrine:migrations:migrate
|
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
# ساخت migration بعد از تغییر entity
|
||||||
|
ddev exec php bin/console doctrine:migrations:diff --no-interaction
|
||||||
|
|
||||||
# هش کردن پسورد
|
# هش کردن پسورد
|
||||||
ddev exec php bin/console security:hash-password "your_password"
|
ddev exec php bin/console security:hash-password "رمز-جدید"
|
||||||
|
|
||||||
# تولید JWT key
|
# تولید JWT key
|
||||||
ddev exec php bin/console lexik:jwt:generate-keypair
|
ddev exec php bin/console lexik:jwt:generate-keypair
|
||||||
|
|
||||||
|
# بررسی صف پیام
|
||||||
|
ddev exec php bin/console messenger:consume async
|
||||||
|
|
||||||
|
# خروجی OpenAPI/Swagger
|
||||||
|
ddev exec php bin/console nelmio:apidoc:dump
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ۱۲. Migrations
|
## ۱۲. Migrations
|
||||||
|
|
||||||
**تعداد:** ۱۶ migration
|
**تعداد:** ۱۷ migration
|
||||||
**آخرین نسخه:** `Version20260609140423`
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# مشاهده وضعیت
|
# مشاهده وضعیت
|
||||||
@@ -926,7 +976,7 @@ ddev exec php bin/console doctrine:migrations:status
|
|||||||
# اجرای migrations جدید
|
# اجرای migrations جدید
|
||||||
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
|
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
# ساخت migration جدید
|
# ساخت migration جدید بعد از تغییر entity
|
||||||
ddev exec php bin/console doctrine:migrations:diff
|
ddev exec php bin/console doctrine:migrations:diff
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -941,8 +991,6 @@ ddev exec php bin/console doctrine:migrations:diff
|
|||||||
| بانک ملت | `MellatGateway` | `MELLAT_TERMINAL_ID`, `MELLAT_USERNAME`, `MELLAT_PASSWORD` |
|
| بانک ملت | `MellatGateway` | `MELLAT_TERMINAL_ID`, `MELLAT_USERNAME`, `MELLAT_PASSWORD` |
|
||||||
| سامان (SEP) | `SepGateway` | `SEP_TERMINAL_ID` |
|
| سامان (SEP) | `SepGateway` | `SEP_TERMINAL_ID` |
|
||||||
|
|
||||||
**IPهای مجاز callback:** در `ALLOWED_FRONTEND_HOSTS` تنظیم میشود.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### SMS Providers
|
### SMS Providers
|
||||||
@@ -956,13 +1004,14 @@ ddev exec php bin/console doctrine:migrations:diff
|
|||||||
|
|
||||||
### Redis
|
### Redis
|
||||||
- صف پیام (Symfony Messenger)
|
- صف پیام (Symfony Messenger)
|
||||||
- کش OTP codes
|
- کش کدهای OTP
|
||||||
- کش Refresh Tokens
|
- کش Refresh Tokens
|
||||||
- Rate limiter storage
|
- Rate limiter storage
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### JWT Keys
|
### JWT Keys
|
||||||
|
|
||||||
```
|
```
|
||||||
config/jwt/private.pem ← کلید خصوصی (در .gitignore)
|
config/jwt/private.pem ← کلید خصوصی (در .gitignore)
|
||||||
config/jwt/public.pem ← کلید عمومی (در .gitignore)
|
config/jwt/public.pem ← کلید عمومی (در .gitignore)
|
||||||
@@ -976,10 +1025,11 @@ config/jwt/public.pem ← کلید عمومی (در .gitignore)
|
|||||||
|-------|-------|
|
|-------|-------|
|
||||||
| Domain modules | ۱۵ |
|
| Domain modules | ۱۵ |
|
||||||
| PHP Controllers | ۱۷ |
|
| PHP Controllers | ۱۷ |
|
||||||
| API Endpoints | ۱۳۰+ |
|
| API Endpoints | ۹۷ (مستندسازیشده در Swagger) |
|
||||||
| Doctrine Entities | ۲۲ |
|
| Doctrine Entities | ۲۲ |
|
||||||
| Database Tables | ۳۰ |
|
| Database Tables | ۳۰ |
|
||||||
| Migrations | ۱۶ |
|
| Migrations | ۱۷ |
|
||||||
| React Pages | ۲ (پیادهسازیشده) + ۱۳ (در حال توسعه) |
|
| React Pages | ۲۳ مسیر (کاملاً پیادهسازیشده) |
|
||||||
|
| OpenAPI Tags | ۱۱ گروه |
|
||||||
| npm packages | ۳۴ |
|
| npm packages | ۳۴ |
|
||||||
| composer packages | ۲۸ |
|
| composer packages | ۲۸ |
|
||||||
@@ -12,8 +12,11 @@ nelmio_api_doc:
|
|||||||
bearerFormat: JWT
|
bearerFormat: JWT
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
|
html_config:
|
||||||
|
assets_mode: bundle
|
||||||
areas:
|
areas:
|
||||||
path_patterns:
|
default:
|
||||||
- ^/api
|
path_patterns:
|
||||||
- ^/oauth
|
- ^/api
|
||||||
- ^/health
|
- ^/oauth
|
||||||
|
- ^/health
|
||||||
|
|||||||
@@ -2,12 +2,20 @@ security:
|
|||||||
password_hashers:
|
password_hashers:
|
||||||
App\Auth\Entity\User:
|
App\Auth\Entity\User:
|
||||||
algorithm: auto
|
algorithm: auto
|
||||||
|
Symfony\Component\Security\Core\User\InMemoryUser:
|
||||||
|
algorithm: bcrypt
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
app_user_provider:
|
app_user_provider:
|
||||||
entity:
|
entity:
|
||||||
class: App\Auth\Entity\User
|
class: App\Auth\Entity\User
|
||||||
property: mobileNumber
|
property: mobileNumber
|
||||||
|
api_doc_provider:
|
||||||
|
memory:
|
||||||
|
users:
|
||||||
|
'%env(API_DOC_USERNAME)%':
|
||||||
|
password: '%env(API_DOC_PASSWORD)%'
|
||||||
|
roles: ['ROLE_ADMIN']
|
||||||
|
|
||||||
firewalls:
|
firewalls:
|
||||||
dev:
|
dev:
|
||||||
@@ -18,6 +26,12 @@ security:
|
|||||||
pattern: ^/health$
|
pattern: ^/health$
|
||||||
security: false
|
security: false
|
||||||
|
|
||||||
|
api_doc:
|
||||||
|
pattern: ^/api/doc
|
||||||
|
http_basic:
|
||||||
|
realm: "ClinicPro API Documentation"
|
||||||
|
provider: api_doc_provider
|
||||||
|
|
||||||
public_endpoints:
|
public_endpoints:
|
||||||
pattern: ^/(api/v1/user/(send-code|verify-code|register)|oauth/token$|session/token|api/v1/categorys/|api/v1/doctors$|api/v1/clinics$|api/v1/clinic/doctor-list/|api/v1/clinic-pro/doctor-addresses/|api/v1/appointment-slots|api/v1/comments/|api/v1/rate/|api/v1/blogs$)
|
pattern: ^/(api/v1/user/(send-code|verify-code|register)|oauth/token$|session/token|api/v1/categorys/|api/v1/doctors$|api/v1/clinics$|api/v1/clinic/doctor-list/|api/v1/clinic-pro/doctor-addresses/|api/v1/appointment-slots|api/v1/comments/|api/v1/rate/|api/v1/blogs$)
|
||||||
stateless: true
|
stateless: true
|
||||||
@@ -38,6 +52,7 @@ security:
|
|||||||
|
|
||||||
access_control:
|
access_control:
|
||||||
- { path: ^/health$, roles: PUBLIC_ACCESS }
|
- { path: ^/health$, roles: PUBLIC_ACCESS }
|
||||||
|
- { path: ^/api/doc, roles: ROLE_ADMIN }
|
||||||
- { path: ^/api/v1/user/send-code, roles: PUBLIC_ACCESS }
|
- { path: ^/api/v1/user/send-code, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/v1/user/verify-code, roles: PUBLIC_ACCESS }
|
- { path: ^/api/v1/user/verify-code, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/v1/user/register, roles: PUBLIC_ACCESS }
|
- { path: ^/api/v1/user/register, roles: PUBLIC_ACCESS }
|
||||||
@@ -54,15 +69,14 @@ security:
|
|||||||
- { path: ^/session/token, roles: PUBLIC_ACCESS }
|
- { path: ^/session/token, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/v1/payment/callback/, roles: PUBLIC_ACCESS }
|
- { path: ^/api/v1/payment/callback/, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/v1/subscription-payment/callback/, roles: PUBLIC_ACCESS }
|
- { path: ^/api/v1/subscription-payment/callback/, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/doc, roles: PUBLIC_ACCESS }
|
- { path: ^/api/v1/categorys/, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/v1/categorys/, roles: PUBLIC_ACCESS }
|
- { path: ^/api/v1/doctors$, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/v1/doctors$, roles: PUBLIC_ACCESS }
|
|
||||||
- path: '^/api/v1/doctor/[^/]+$'
|
- path: '^/api/v1/doctor/[^/]+$'
|
||||||
methods: [GET]
|
methods: [GET]
|
||||||
roles: PUBLIC_ACCESS
|
roles: PUBLIC_ACCESS
|
||||||
- { path: ^/api/v1/clinic/doctor-list/, roles: PUBLIC_ACCESS }
|
- { path: ^/api/v1/clinic/doctor-list/, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/v1/clinic-pro/doctor-addresses/, roles: PUBLIC_ACCESS }
|
- { path: ^/api/v1/clinic-pro/doctor-addresses/, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/v1/clinics$, roles: PUBLIC_ACCESS }
|
- { path: ^/api/v1/clinics$, roles: PUBLIC_ACCESS }
|
||||||
- path: '^/api/v1/clinic/[^/]+$'
|
- path: '^/api/v1/clinic/[^/]+$'
|
||||||
methods: [GET]
|
methods: [GET]
|
||||||
roles: PUBLIC_ACCESS
|
roles: PUBLIC_ACCESS
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ use App\Sms\Entity\SmsLog;
|
|||||||
use App\Sms\Entity\SmsTemplate;
|
use App\Sms\Entity\SmsTemplate;
|
||||||
use App\Shared\Controller\BaseController;
|
use App\Shared\Controller\BaseController;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[OA\Tag(name: 'Admin')]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
class AdminApiController extends BaseController
|
class AdminApiController extends BaseController
|
||||||
{
|
{
|
||||||
@@ -31,6 +33,46 @@ class AdminApiController extends BaseController
|
|||||||
|
|
||||||
// ── Users ─────────────────────────────────────────────────────────────────
|
// ── Users ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/admin/users',
|
||||||
|
summary: 'List all users (paginated)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
|
||||||
|
new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||||
|
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Paginated list of users',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string'),
|
||||||
|
new OA\Property(property: 'id', type: 'integer'),
|
||||||
|
new OA\Property(property: 'mobile_number', type: 'string'),
|
||||||
|
new OA\Property(property: 'name', type: 'string'),
|
||||||
|
new OA\Property(property: 'email', type: 'string'),
|
||||||
|
new OA\Property(property: 'roles', type: 'array', items: new OA\Items(type: 'string')),
|
||||||
|
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||||
|
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
new OA\Property(property: 'meta', properties: [
|
||||||
|
new OA\Property(property: 'totalRecords', type: 'integer'),
|
||||||
|
new OA\Property(property: 'totalPages', type: 'integer'),
|
||||||
|
new OA\Property(property: 'currentPage', type: 'integer'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/admin/users', methods: ['GET'])]
|
#[Route('/api/v1/admin/users', methods: ['GET'])]
|
||||||
public function users(Request $request): JsonResponse
|
public function users(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -69,6 +111,48 @@ class AdminApiController extends BaseController
|
|||||||
|
|
||||||
// ── Appointments ──────────────────────────────────────────────────────────
|
// ── Appointments ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/admin/appointments',
|
||||||
|
summary: 'List all appointments (paginated)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
|
||||||
|
new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||||
|
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||||
|
new OA\Parameter(name: 'status', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Paginated list of appointments',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string'),
|
||||||
|
new OA\Property(property: 'patient_name', type: 'string'),
|
||||||
|
new OA\Property(property: 'patient_mobile', type: 'string'),
|
||||||
|
new OA\Property(property: 'doctor_name', type: 'string'),
|
||||||
|
new OA\Property(property: 'appointment_date', type: 'string', format: 'date'),
|
||||||
|
new OA\Property(property: 'appointment_time', type: 'string', example: '14:30'),
|
||||||
|
new OA\Property(property: 'status', type: 'string'),
|
||||||
|
new OA\Property(property: 'amount', type: 'integer'),
|
||||||
|
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
new OA\Property(property: 'meta', properties: [
|
||||||
|
new OA\Property(property: 'totalRecords', type: 'integer'),
|
||||||
|
new OA\Property(property: 'totalPages', type: 'integer'),
|
||||||
|
new OA\Property(property: 'currentPage', type: 'integer'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/admin/appointments', methods: ['GET'])]
|
#[Route('/api/v1/admin/appointments', methods: ['GET'])]
|
||||||
public function appointments(Request $request): JsonResponse
|
public function appointments(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -118,6 +202,46 @@ class AdminApiController extends BaseController
|
|||||||
|
|
||||||
// ── Payments ──────────────────────────────────────────────────────────────
|
// ── Payments ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/admin/payments',
|
||||||
|
summary: 'List all payments (paginated)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
|
||||||
|
new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||||
|
new OA\Parameter(name: 'status', in: 'query', required: false, schema: new OA\Schema(type: 'string', enum: ['pending', 'success', 'failed'])),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Paginated list of payments',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string'),
|
||||||
|
new OA\Property(property: 'amount', type: 'integer'),
|
||||||
|
new OA\Property(property: 'status', type: 'string'),
|
||||||
|
new OA\Property(property: 'gateway', type: 'string'),
|
||||||
|
new OA\Property(property: 'ref_id', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'patient_mobile', type: 'string'),
|
||||||
|
new OA\Property(property: 'paid_at', type: 'string', format: 'date-time', nullable: true),
|
||||||
|
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
new OA\Property(property: 'meta', properties: [
|
||||||
|
new OA\Property(property: 'totalRecords', type: 'integer'),
|
||||||
|
new OA\Property(property: 'totalPages', type: 'integer'),
|
||||||
|
new OA\Property(property: 'currentPage', type: 'integer'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/admin/payments', methods: ['GET'])]
|
#[Route('/api/v1/admin/payments', methods: ['GET'])]
|
||||||
public function payments(Request $request): JsonResponse
|
public function payments(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -164,6 +288,49 @@ class AdminApiController extends BaseController
|
|||||||
|
|
||||||
// ── Representations ───────────────────────────────────────────────────────
|
// ── Representations ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/admin/representations',
|
||||||
|
summary: 'List all representations (paginated)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
|
||||||
|
new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||||
|
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||||
|
new OA\Parameter(name: 'city_id', in: 'query', required: false, schema: new OA\Schema(type: 'integer')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Paginated list of representations',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string'),
|
||||||
|
new OA\Property(property: 'domain', type: 'string'),
|
||||||
|
new OA\Property(property: 'full_name', type: 'string'),
|
||||||
|
new OA\Property(property: 'mobile_number', type: 'string'),
|
||||||
|
new OA\Property(property: 'city_id', type: 'integer', nullable: true),
|
||||||
|
new OA\Property(property: 'city', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'commission_percent', type: 'number', format: 'float'),
|
||||||
|
new OA\Property(property: 'wallet_balance', type: 'integer'),
|
||||||
|
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||||
|
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
new OA\Property(property: 'meta', properties: [
|
||||||
|
new OA\Property(property: 'totalRecords', type: 'integer'),
|
||||||
|
new OA\Property(property: 'totalPages', type: 'integer'),
|
||||||
|
new OA\Property(property: 'currentPage', type: 'integer'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/admin/representations', methods: ['GET'])]
|
#[Route('/api/v1/admin/representations', methods: ['GET'])]
|
||||||
public function representations(Request $request): JsonResponse
|
public function representations(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -211,6 +378,46 @@ class AdminApiController extends BaseController
|
|||||||
|
|
||||||
// ── Secretaries ───────────────────────────────────────────────────────────
|
// ── Secretaries ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/admin/secretaries',
|
||||||
|
summary: 'List all secretaries (paginated)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
|
||||||
|
new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||||
|
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Paginated list of secretaries',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string'),
|
||||||
|
new OA\Property(property: 'user_name', type: 'string'),
|
||||||
|
new OA\Property(property: 'mobile_number', type: 'string'),
|
||||||
|
new OA\Property(property: 'doctor_name', type: 'string'),
|
||||||
|
new OA\Property(property: 'doctor_uuid', type: 'string'),
|
||||||
|
new OA\Property(property: 'is_active', type: 'boolean'),
|
||||||
|
new OA\Property(property: 'permissions', type: 'array', items: new OA\Items(type: 'string')),
|
||||||
|
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
new OA\Property(property: 'meta', properties: [
|
||||||
|
new OA\Property(property: 'totalRecords', type: 'integer'),
|
||||||
|
new OA\Property(property: 'totalPages', type: 'integer'),
|
||||||
|
new OA\Property(property: 'currentPage', type: 'integer'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/admin/secretaries', methods: ['GET'])]
|
#[Route('/api/v1/admin/secretaries', methods: ['GET'])]
|
||||||
public function secretaries(Request $request): JsonResponse
|
public function secretaries(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -255,6 +462,44 @@ class AdminApiController extends BaseController
|
|||||||
|
|
||||||
// ── Ratings ───────────────────────────────────────────────────────────────
|
// ── Ratings ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/admin/rates',
|
||||||
|
summary: 'List all ratings (paginated)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
|
||||||
|
new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||||
|
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Paginated list of ratings',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string'),
|
||||||
|
new OA\Property(property: 'patient_name', type: 'string'),
|
||||||
|
new OA\Property(property: 'doctor_name', type: 'string'),
|
||||||
|
new OA\Property(property: 'overall', type: 'integer'),
|
||||||
|
new OA\Property(property: 'score', type: 'integer'),
|
||||||
|
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
new OA\Property(property: 'meta', properties: [
|
||||||
|
new OA\Property(property: 'totalRecords', type: 'integer'),
|
||||||
|
new OA\Property(property: 'totalPages', type: 'integer'),
|
||||||
|
new OA\Property(property: 'currentPage', type: 'integer'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/admin/rates', methods: ['GET'])]
|
#[Route('/api/v1/admin/rates', methods: ['GET'])]
|
||||||
public function rates(Request $request): JsonResponse
|
public function rates(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -297,6 +542,47 @@ class AdminApiController extends BaseController
|
|||||||
|
|
||||||
// ── Comments ──────────────────────────────────────────────────────────────
|
// ── Comments ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/admin/comments',
|
||||||
|
summary: 'List all comments (paginated)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
|
||||||
|
new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||||
|
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||||
|
new OA\Parameter(name: 'status', in: 'query', required: false, schema: new OA\Schema(type: 'string', enum: ['pending', 'approved', 'rejected'])),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Paginated list of comments',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string'),
|
||||||
|
new OA\Property(property: 'patient_name', type: 'string'),
|
||||||
|
new OA\Property(property: 'doctor_name', type: 'string'),
|
||||||
|
new OA\Property(property: 'title', type: 'string'),
|
||||||
|
new OA\Property(property: 'body', type: 'string'),
|
||||||
|
new OA\Property(property: 'is_approved', type: 'boolean'),
|
||||||
|
new OA\Property(property: 'status', type: 'string'),
|
||||||
|
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
new OA\Property(property: 'meta', properties: [
|
||||||
|
new OA\Property(property: 'totalRecords', type: 'integer'),
|
||||||
|
new OA\Property(property: 'totalPages', type: 'integer'),
|
||||||
|
new OA\Property(property: 'currentPage', type: 'integer'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/admin/comments', methods: ['GET'])]
|
#[Route('/api/v1/admin/comments', methods: ['GET'])]
|
||||||
public function comments(Request $request): JsonResponse
|
public function comments(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -345,6 +631,44 @@ class AdminApiController extends BaseController
|
|||||||
|
|
||||||
// ── SMS Logs ──────────────────────────────────────────────────────────────
|
// ── SMS Logs ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/admin/sms/logs',
|
||||||
|
summary: 'List SMS send logs (paginated)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
|
||||||
|
new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Paginated list of SMS logs',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string'),
|
||||||
|
new OA\Property(property: 'recipient', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
new OA\Property(property: 'status', type: 'string', enum: ['sent', 'failed']),
|
||||||
|
new OA\Property(property: 'provider', type: 'string'),
|
||||||
|
new OA\Property(property: 'sent_at', type: 'string', format: 'date-time'),
|
||||||
|
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
new OA\Property(property: 'meta', properties: [
|
||||||
|
new OA\Property(property: 'totalRecords', type: 'integer'),
|
||||||
|
new OA\Property(property: 'totalPages', type: 'integer'),
|
||||||
|
new OA\Property(property: 'currentPage', type: 'integer'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/admin/sms/logs', methods: ['GET'])]
|
#[Route('/api/v1/admin/sms/logs', methods: ['GET'])]
|
||||||
public function smsLogs(Request $request): JsonResponse
|
public function smsLogs(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -376,6 +700,47 @@ class AdminApiController extends BaseController
|
|||||||
|
|
||||||
// ── Settlements ───────────────────────────────────────────────────────────
|
// ── Settlements ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/admin/settlements',
|
||||||
|
summary: 'List all settlement requests (paginated)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
|
||||||
|
new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||||
|
new OA\Parameter(name: 'status', in: 'query', required: false, schema: new OA\Schema(type: 'string', enum: ['pending', 'approved', 'rejected'])),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Paginated list of settlement requests',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string'),
|
||||||
|
new OA\Property(property: 'representation_name', type: 'string'),
|
||||||
|
new OA\Property(property: 'amount', type: 'integer'),
|
||||||
|
new OA\Property(property: 'status', type: 'string'),
|
||||||
|
new OA\Property(property: 'bank_card', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'bank_name', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'reject_reason', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'requested_at', type: 'string', format: 'date-time'),
|
||||||
|
new OA\Property(property: 'processed_at', type: 'string', format: 'date-time', nullable: true),
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
new OA\Property(property: 'meta', properties: [
|
||||||
|
new OA\Property(property: 'totalRecords', type: 'integer'),
|
||||||
|
new OA\Property(property: 'totalPages', type: 'integer'),
|
||||||
|
new OA\Property(property: 'currentPage', type: 'integer'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/admin/settlements', methods: ['GET'])]
|
#[Route('/api/v1/admin/settlements', methods: ['GET'])]
|
||||||
public function settlements(Request $request): JsonResponse
|
public function settlements(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -418,6 +783,45 @@ class AdminApiController extends BaseController
|
|||||||
|
|
||||||
// ── SMS Templates (paginated list with optional status filter) ────────────
|
// ── SMS Templates (paginated list with optional status filter) ────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/admin/sms/sample-templates',
|
||||||
|
summary: 'List SMS sample templates (paginated)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
|
||||||
|
new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||||
|
new OA\Parameter(name: 'status', in: 'query', required: false, schema: new OA\Schema(type: 'string', enum: ['approved', 'pending'], default: 'approved')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Paginated list of SMS templates',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string'),
|
||||||
|
new OA\Property(property: 'name', type: 'string'),
|
||||||
|
new OA\Property(property: 'body', type: 'string'),
|
||||||
|
new OA\Property(property: 'provider_code', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'status', type: 'string'),
|
||||||
|
new OA\Property(property: 'admin_note', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
new OA\Property(property: 'meta', properties: [
|
||||||
|
new OA\Property(property: 'totalRecords', type: 'integer'),
|
||||||
|
new OA\Property(property: 'totalPages', type: 'integer'),
|
||||||
|
new OA\Property(property: 'currentPage', type: 'integer'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/admin/sms/sample-templates', methods: ['GET'])]
|
#[Route('/api/v1/admin/sms/sample-templates', methods: ['GET'])]
|
||||||
public function smsSampleTemplates(Request $request): JsonResponse
|
public function smsSampleTemplates(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -454,6 +858,57 @@ class AdminApiController extends BaseController
|
|||||||
|
|
||||||
// ── Dashboard Recent ──────────────────────────────────────────────────────
|
// ── Dashboard Recent ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/admin/dashboard/recent',
|
||||||
|
summary: 'Get recent dashboard data (appointments, payments, users)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Recent dashboard data',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', properties: [
|
||||||
|
new OA\Property(property: 'appointments', type: 'array', items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string'),
|
||||||
|
new OA\Property(property: 'slot_start', type: 'string', format: 'date-time'),
|
||||||
|
new OA\Property(property: 'status', type: 'string'),
|
||||||
|
new OA\Property(property: 'doctor_name', type: 'string'),
|
||||||
|
new OA\Property(property: 'user_mobile', type: 'string'),
|
||||||
|
new OA\Property(property: 'user_name', type: 'string'),
|
||||||
|
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
new OA\Property(property: 'payments', type: 'array', items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string'),
|
||||||
|
new OA\Property(property: 'amount', type: 'integer'),
|
||||||
|
new OA\Property(property: 'status', type: 'string'),
|
||||||
|
new OA\Property(property: 'gateway', type: 'string'),
|
||||||
|
new OA\Property(property: 'user_mobile', type: 'string'),
|
||||||
|
new OA\Property(property: 'user_name', type: 'string'),
|
||||||
|
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
new OA\Property(property: 'users', type: 'array', items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string'),
|
||||||
|
new OA\Property(property: 'mobile', type: 'string'),
|
||||||
|
new OA\Property(property: 'name', type: 'string'),
|
||||||
|
new OA\Property(property: 'email', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/admin/dashboard/recent', methods: ['GET'])]
|
#[Route('/api/v1/admin/dashboard/recent', methods: ['GET'])]
|
||||||
public function dashboardRecent(): JsonResponse
|
public function dashboardRecent(): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -521,6 +976,37 @@ class AdminApiController extends BaseController
|
|||||||
|
|
||||||
// ── Dashboard Stats ───────────────────────────────────────────────────────
|
// ── Dashboard Stats ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/admin/dashboard/stats',
|
||||||
|
summary: 'Get aggregated dashboard statistics',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Dashboard statistics',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', properties: [
|
||||||
|
new OA\Property(property: 'total_users', type: 'integer'),
|
||||||
|
new OA\Property(property: 'active_doctors', type: 'integer'),
|
||||||
|
new OA\Property(property: 'total_doctors', type: 'integer'),
|
||||||
|
new OA\Property(property: 'total_clinics', type: 'integer'),
|
||||||
|
new OA\Property(property: 'today_appointments', type: 'integer'),
|
||||||
|
new OA\Property(property: 'total_appointments', type: 'integer'),
|
||||||
|
new OA\Property(property: 'today_payments_count', type: 'integer'),
|
||||||
|
new OA\Property(property: 'today_payments_amount', type: 'integer'),
|
||||||
|
new OA\Property(property: 'total_payments_amount', type: 'integer'),
|
||||||
|
new OA\Property(property: 'pending_comments', type: 'integer'),
|
||||||
|
new OA\Property(property: 'pending_settlements', type: 'integer'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/admin/dashboard/stats', methods: ['GET'])]
|
#[Route('/api/v1/admin/dashboard/stats', methods: ['GET'])]
|
||||||
public function dashboardStats(): JsonResponse
|
public function dashboardStats(): JsonResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ use App\Doctor\Repository\DoctorRepository;
|
|||||||
use App\Shared\Constant\ErrorCodes;
|
use App\Shared\Constant\ErrorCodes;
|
||||||
use App\Shared\Controller\BaseController;
|
use App\Shared\Controller\BaseController;
|
||||||
use Doctrine\ORM\OptimisticLockException;
|
use Doctrine\ORM\OptimisticLockException;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[OA\Tag(name: 'Appointments')]
|
||||||
class AppointmentController extends BaseController
|
class AppointmentController extends BaseController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -26,6 +28,78 @@ class AppointmentController extends BaseController
|
|||||||
|
|
||||||
// ── Public: available slots ───────────────────────────────────────────────
|
// ── Public: available slots ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/appointment-slots',
|
||||||
|
summary: 'Get available appointment slots for a doctor on a given date',
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'doctor_uuid',
|
||||||
|
in: 'query',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||||
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'date',
|
||||||
|
in: 'query',
|
||||||
|
required: true,
|
||||||
|
description: 'Date in Y-m-d format',
|
||||||
|
schema: new OA\Schema(type: 'string', format: 'date', example: '2025-06-15')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Available slots returned',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'doctor_uuid', type: 'string', format: 'uuid'),
|
||||||
|
new OA\Property(property: 'date', type: 'string', format: 'date'),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'slots',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'start', type: 'integer', description: 'Unix timestamp'),
|
||||||
|
new OA\Property(property: 'end', type: 'integer', description: 'Unix timestamp'),
|
||||||
|
new OA\Property(property: 'available', type: 'boolean'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 404,
|
||||||
|
description: 'Doctor not found',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'errors',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 422, description: 'Invalid date format'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/appointment-slots', methods: ['GET'])]
|
#[Route('/api/v1/appointment-slots', methods: ['GET'])]
|
||||||
public function slots(Request $request): JsonResponse
|
public function slots(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -52,6 +126,40 @@ class AppointmentController extends BaseController
|
|||||||
|
|
||||||
// ── Authenticated: book / manage ─────────────────────────────────────────
|
// ── Authenticated: book / manage ─────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/appointment',
|
||||||
|
summary: 'Book a new appointment',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['doctor_uuid', 'slot_start', 'slot_end'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'doctor_uuid', type: 'string', format: 'uuid'),
|
||||||
|
new OA\Property(property: 'slot_start', type: 'integer', description: 'Slot start Unix timestamp'),
|
||||||
|
new OA\Property(property: 'slot_end', type: 'integer', description: 'Slot end Unix timestamp'),
|
||||||
|
new OA\Property(property: 'note', type: 'string'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 201,
|
||||||
|
description: 'Appointment booked successfully',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object', description: 'Appointment object'),
|
||||||
|
new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthenticated'),
|
||||||
|
new OA\Response(response: 404, description: 'Doctor not found'),
|
||||||
|
new OA\Response(response: 409, description: 'Slot already taken'),
|
||||||
|
new OA\Response(response: 422, description: 'Validation error'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
#[Route('/api/v1/appointment', methods: ['POST'])]
|
#[Route('/api/v1/appointment', methods: ['POST'])]
|
||||||
public function book(Request $request, #[CurrentUser] User $user): JsonResponse
|
public function book(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -82,6 +190,35 @@ class AppointmentController extends BaseController
|
|||||||
return $this->success(['data' => $appointment->toArray()], 201);
|
return $this->success(['data' => $appointment->toArray()], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/appointment/{uuid}',
|
||||||
|
summary: 'Get a single appointment by UUID',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Appointment returned',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object', description: 'Appointment object'),
|
||||||
|
new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthenticated'),
|
||||||
|
new OA\Response(response: 403, description: 'Access denied'),
|
||||||
|
new OA\Response(response: 404, description: 'Appointment not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
#[Route('/api/v1/appointment/{uuid}', methods: ['GET'])]
|
#[Route('/api/v1/appointment/{uuid}', methods: ['GET'])]
|
||||||
public function get(string $uuid, #[CurrentUser] User $user): JsonResponse
|
public function get(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -98,6 +235,46 @@ class AppointmentController extends BaseController
|
|||||||
return $this->success(['data' => $appointment->toArray()]);
|
return $this->success(['data' => $appointment->toArray()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/appointments/doctor/{doctorUuid}',
|
||||||
|
summary: 'List appointments for a specific doctor',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'doctorUuid',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||||
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'status',
|
||||||
|
in: 'query',
|
||||||
|
required: false,
|
||||||
|
description: 'Filter by appointment status',
|
||||||
|
schema: new OA\Schema(type: 'string', example: 'pending')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Appointment list returned',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(type: 'object', description: 'Appointment object')
|
||||||
|
),
|
||||||
|
new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthenticated'),
|
||||||
|
new OA\Response(response: 403, description: 'Access denied'),
|
||||||
|
new OA\Response(response: 404, description: 'Doctor not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
#[Route('/api/v1/appointments/doctor/{doctorUuid}', methods: ['GET'])]
|
#[Route('/api/v1/appointments/doctor/{doctorUuid}', methods: ['GET'])]
|
||||||
public function listByDoctor(string $doctorUuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
public function listByDoctor(string $doctorUuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -117,6 +294,38 @@ class AppointmentController extends BaseController
|
|||||||
return $this->success(['data' => array_map(fn(Appointment $a) => $a->toArray(), $appointments)]);
|
return $this->success(['data' => array_map(fn(Appointment $a) => $a->toArray(), $appointments)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/appointments/user',
|
||||||
|
summary: 'List appointments for the authenticated user',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'status',
|
||||||
|
in: 'query',
|
||||||
|
required: false,
|
||||||
|
description: 'Filter by appointment status',
|
||||||
|
schema: new OA\Schema(type: 'string', example: 'pending')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Appointment list returned',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(type: 'object', description: 'Appointment object')
|
||||||
|
),
|
||||||
|
new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthenticated'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
#[Route('/api/v1/appointments/user', methods: ['GET'])]
|
#[Route('/api/v1/appointments/user', methods: ['GET'])]
|
||||||
public function listByUser(Request $request, #[CurrentUser] User $user): JsonResponse
|
public function listByUser(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -141,6 +350,47 @@ class AppointmentController extends BaseController
|
|||||||
|| $user->hasRole('ROLE_ADMIN');
|
|| $user->hasRole('ROLE_ADMIN');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Patch(
|
||||||
|
path: '/api/v1/appointment/{uuid}/status',
|
||||||
|
summary: 'Update the status of an appointment',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['status'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'status', type: 'string', example: 'confirmed'),
|
||||||
|
new OA\Property(property: 'version', type: 'integer', description: 'Optimistic lock version'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Appointment status updated',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object', description: 'Updated appointment object'),
|
||||||
|
new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthenticated'),
|
||||||
|
new OA\Response(response: 403, description: 'Access denied'),
|
||||||
|
new OA\Response(response: 404, description: 'Appointment not found'),
|
||||||
|
new OA\Response(response: 409, description: 'Optimistic lock conflict'),
|
||||||
|
new OA\Response(response: 422, description: 'Invalid status transition'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
#[Route('/api/v1/appointment/{uuid}/status', methods: ['PATCH'])]
|
#[Route('/api/v1/appointment/{uuid}/status', methods: ['PATCH'])]
|
||||||
public function updateStatus(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
public function updateStatus(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ use App\Auth\Service\OtpService;
|
|||||||
use App\Auth\Service\TokenService;
|
use App\Auth\Service\TokenService;
|
||||||
use App\Shared\Constant\ErrorCodes;
|
use App\Shared\Constant\ErrorCodes;
|
||||||
use App\Shared\Controller\BaseController;
|
use App\Shared\Controller\BaseController;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\RateLimiter\RateLimiterFactory;
|
use Symfony\Component\RateLimiter\RateLimiterFactory;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||||
|
|
||||||
|
#[OA\Tag(name: 'Auth')]
|
||||||
class AuthController extends BaseController
|
class AuthController extends BaseController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -27,12 +29,110 @@ class AuthController extends BaseController
|
|||||||
* Route exists so the router resolves it; PasswordAuthenticator intercepts
|
* Route exists so the router resolves it; PasswordAuthenticator intercepts
|
||||||
* and returns the JWT response before this controller body ever runs.
|
* and returns the JWT response before this controller body ever runs.
|
||||||
*/
|
*/
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/user/login',
|
||||||
|
summary: 'Staff login (Admin / Doctor / Clinic / Secretary)',
|
||||||
|
description: 'ورود با شماره موبایل و رمز عبور — فقط برای کاربران دارای نقش ROLE_ADMIN، ROLE_DOCTOR، ROLE_CLINIC یا ROLE_SECRETARY. کاربران عادی باید از OTP استفاده کنند.',
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['mobile_number', 'password'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'mobile_number', type: 'string', example: '09120671713', description: 'شماره موبایل ثبتشده'),
|
||||||
|
new OA\Property(property: 'password', type: 'string', format: 'password', example: 'admin1234', description: 'رمز عبور'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'ورود موفق — JWT و refresh token برگردانده میشود',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'access_token', type: 'string', description: 'JWT — عمر ۱ ساعت'),
|
||||||
|
new OA\Property(property: 'refresh_token', type: 'string', description: 'Refresh token — عمر ۳۰ روز'),
|
||||||
|
new OA\Property(property: 'token_type', type: 'string', example: 'Bearer'),
|
||||||
|
new OA\Property(property: 'expires_in', type: 'integer', example: 3600),
|
||||||
|
new OA\Property(property: 'refresh_token_expires_in', type: 'integer', example: 2592000),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 401,
|
||||||
|
description: 'اطلاعات ورود نادرست',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'errors',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string', example: 'ERR_AUTH_005'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 403, description: 'کاربر نقش staff ندارد (ROLE_ADMIN/ROLE_DOCTOR/ROLE_CLINIC/ROLE_SECRETARY)'),
|
||||||
|
new OA\Response(response: 429, description: 'تعداد تلاشهای ورود از حد مجاز گذشت'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/user/login', methods: ['POST'])]
|
#[Route('/api/v1/user/login', methods: ['POST'])]
|
||||||
public function login(): JsonResponse
|
public function login(): JsonResponse
|
||||||
{
|
{
|
||||||
return $this->error(ErrorCodes::ERR_AUTH_005, ErrorCodes::message(ErrorCodes::ERR_AUTH_005), 401);
|
return $this->error(ErrorCodes::ERR_AUTH_005, ErrorCodes::message(ErrorCodes::ERR_AUTH_005), 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/user/send-code',
|
||||||
|
summary: 'Send OTP code to mobile number',
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['mobile'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'mobile', type: 'string', example: '09123456789'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'OTP sent successfully',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||||
|
new OA\Property(property: 'message', type: 'string', example: 'کد تایید با موفقیت ارسال شد.'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 422,
|
||||||
|
description: 'Invalid mobile format',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'errors',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 429, description: 'Rate limit exceeded'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/user/send-code', methods: ['POST'])]
|
#[Route('/api/v1/user/send-code', methods: ['POST'])]
|
||||||
public function sendCode(Request $request): JsonResponse
|
public function sendCode(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -53,6 +153,59 @@ class AuthController extends BaseController
|
|||||||
return new JsonResponse(['uuid' => $uuid, 'message' => 'کد تایید با موفقیت ارسال شد.']);
|
return new JsonResponse(['uuid' => $uuid, 'message' => 'کد تایید با موفقیت ارسال شد.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/user/verify-code',
|
||||||
|
summary: 'Verify OTP code',
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['uuid', 'code'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||||
|
new OA\Property(property: 'code', type: 'string', example: '12345'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Code verified successfully',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'message', type: 'string', example: 'کد با موفقیت تایید شد.'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 422,
|
||||||
|
description: 'Missing or invalid uuid/code',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'errors',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/user/verify-code', methods: ['POST'])]
|
#[Route('/api/v1/user/verify-code', methods: ['POST'])]
|
||||||
public function verifyCode(Request $request): JsonResponse
|
public function verifyCode(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -69,6 +222,60 @@ class AuthController extends BaseController
|
|||||||
return $this->success(['message' => 'کد با موفقیت تایید شد.']);
|
return $this->success(['message' => 'کد با موفقیت تایید شد.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/user/register',
|
||||||
|
summary: 'Register a new user after OTP verification',
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['uuid'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||||
|
new OA\Property(property: 'real_name', type: 'string', example: 'علی محمدی'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 201,
|
||||||
|
description: 'User registered successfully',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'message', type: 'string', example: 'ثبتنام با موفقیت انجام شد.'),
|
||||||
|
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 422,
|
||||||
|
description: 'Missing uuid',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'errors',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/user/register', methods: ['POST'])]
|
#[Route('/api/v1/user/register', methods: ['POST'])]
|
||||||
public function register(Request $request): JsonResponse
|
public function register(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -94,6 +301,52 @@ class AuthController extends BaseController
|
|||||||
return $this->success(['message' => 'ثبتنام با موفقیت انجام شد.', 'uuid' => $user->getUuid()], 201);
|
return $this->success(['message' => 'ثبتنام با موفقیت انجام شد.', 'uuid' => $user->getUuid()], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/oauth/token',
|
||||||
|
summary: 'Issue access and refresh tokens using OTP-verified UUID',
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['grant_type', 'uuid'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'grant_type', type: 'string', enum: ['mobile'], example: 'mobile'),
|
||||||
|
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Tokens issued successfully',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'token', type: 'string'),
|
||||||
|
new OA\Property(property: 'refresh_token', type: 'string'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 400,
|
||||||
|
description: 'Invalid grant_type',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'errors',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/oauth/token', methods: ['POST'])]
|
#[Route('/oauth/token', methods: ['POST'])]
|
||||||
public function issueToken(Request $request): JsonResponse
|
public function issueToken(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -115,6 +368,51 @@ class AuthController extends BaseController
|
|||||||
return new JsonResponse($this->tokenService->issueTokens($user));
|
return new JsonResponse($this->tokenService->issueTokens($user));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/oauth/token/refresh',
|
||||||
|
summary: 'Refresh access token using a refresh token',
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['refresh_token'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'refresh_token', type: 'string'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Token refreshed successfully',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'token', type: 'string'),
|
||||||
|
new OA\Property(property: 'refresh_token', type: 'string'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 401,
|
||||||
|
description: 'Invalid or missing refresh token',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'errors',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/oauth/token/refresh', methods: ['POST'])]
|
#[Route('/oauth/token/refresh', methods: ['POST'])]
|
||||||
public function refreshToken(Request $request): JsonResponse
|
public function refreshToken(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -138,6 +436,55 @@ class AuthController extends BaseController
|
|||||||
return new JsonResponse($tokens);
|
return new JsonResponse($tokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/oauth/userinfo',
|
||||||
|
summary: 'Get current authenticated user info',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'User info returned successfully',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'id', type: 'integer'),
|
||||||
|
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||||
|
new OA\Property(property: 'mobile_number', type: 'string'),
|
||||||
|
new OA\Property(property: 'realName', type: 'string'),
|
||||||
|
new OA\Property(property: 'status', type: 'string'),
|
||||||
|
new OA\Property(property: 'roles', type: 'array', items: new OA\Items(type: 'string')),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 401,
|
||||||
|
description: 'Unauthenticated',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'errors',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/oauth/userinfo', methods: ['GET'])]
|
#[Route('/oauth/userinfo', methods: ['GET'])]
|
||||||
public function userInfo(#[CurrentUser] ?User $user): JsonResponse
|
public function userInfo(#[CurrentUser] ?User $user): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -155,6 +502,37 @@ class AuthController extends BaseController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/oauth/logout',
|
||||||
|
summary: 'Logout and optionally revoke refresh token',
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: false,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'refresh_token', type: 'string'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Logged out successfully',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'message', type: 'string', example: 'خروج با موفقیت انجام شد'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/oauth/logout', methods: ['POST'])]
|
#[Route('/oauth/logout', methods: ['POST'])]
|
||||||
public function logout(Request $request): JsonResponse
|
public function logout(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ use App\Blog\Repository\BlogRepository;
|
|||||||
use App\Shared\Constant\ErrorCodes;
|
use App\Shared\Constant\ErrorCodes;
|
||||||
use App\Shared\Controller\BaseController;
|
use App\Shared\Controller\BaseController;
|
||||||
use App\Shared\Service\FileValidatorService;
|
use App\Shared\Service\FileValidatorService;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[OA\Tag(name: 'Blog')]
|
||||||
class BlogController extends BaseController
|
class BlogController extends BaseController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -24,6 +26,35 @@ class BlogController extends BaseController
|
|||||||
|
|
||||||
// ── Public list/detail ────────────────────────────────────────────────────
|
// ── Public list/detail ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/blogs',
|
||||||
|
summary: 'List published blog posts (paginated)',
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
|
||||||
|
new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 20, maximum: 50)),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Paginated list of published blog posts',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(type: 'object')),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'meta',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'totalRecords', type: 'integer'),
|
||||||
|
new OA\Property(property: 'totalPages', type: 'integer'),
|
||||||
|
new OA\Property(property: 'currentPage', type: 'integer'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/blogs', methods: ['GET'])]
|
#[Route('/api/v1/blogs', methods: ['GET'])]
|
||||||
public function list(Request $request): JsonResponse
|
public function list(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -36,6 +67,38 @@ class BlogController extends BaseController
|
|||||||
return $this->paginated($blogs, $total, $page, $limit);
|
return $this->paginated($blogs, $total, $page, $limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/blog/{slug}',
|
||||||
|
summary: 'Get a published blog post by slug or UUID',
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'slug',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
description: 'Blog slug or UUID',
|
||||||
|
schema: new OA\Schema(type: 'string')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Blog post detail',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 404, description: 'Blog post not found or not published'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/blog/{slug}', methods: ['GET'])]
|
#[Route('/api/v1/blog/{slug}', methods: ['GET'])]
|
||||||
public function detail(string $slug): JsonResponse
|
public function detail(string $slug): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -49,6 +112,63 @@ class BlogController extends BaseController
|
|||||||
|
|
||||||
// ── Admin CRUD ────────────────────────────────────────────────────────────
|
// ── Admin CRUD ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/blog',
|
||||||
|
summary: 'Create a new blog post (admin only)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['title', 'body'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'title', type: 'string'),
|
||||||
|
new OA\Property(property: 'body', type: 'string'),
|
||||||
|
new OA\Property(property: 'summary', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'tags', type: 'array', items: new OA\Items(type: 'string'), nullable: true),
|
||||||
|
new OA\Property(property: 'status', type: 'string', enum: ['draft', 'published'], nullable: true),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 201,
|
||||||
|
description: 'Blog post created',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden — admin role required'),
|
||||||
|
new OA\Response(
|
||||||
|
response: 422,
|
||||||
|
description: 'Validation error',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'errors',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/blog', methods: ['POST'])]
|
#[Route('/api/v1/blog', methods: ['POST'])]
|
||||||
public function create(Request $request, #[CurrentUser] User $user): JsonResponse
|
public function create(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -76,6 +196,52 @@ class BlogController extends BaseController
|
|||||||
return $this->success(['data' => $blog->toArray()], 201);
|
return $this->success(['data' => $blog->toArray()], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Patch(
|
||||||
|
path: '/api/v1/blog/{uuid}',
|
||||||
|
summary: 'Update a blog post (admin only)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'title', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'body', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'summary', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'tags', type: 'array', items: new OA\Items(type: 'string'), nullable: true),
|
||||||
|
new OA\Property(property: 'status', type: 'string', enum: ['draft', 'published'], nullable: true),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Blog post updated',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden — admin role required'),
|
||||||
|
new OA\Response(response: 404, description: 'Blog post not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/blog/{uuid}', methods: ['PATCH'])]
|
#[Route('/api/v1/blog/{uuid}', methods: ['PATCH'])]
|
||||||
public function update(string $uuid, Request $request): JsonResponse
|
public function update(string $uuid, Request $request): JsonResponse
|
||||||
@@ -97,6 +263,40 @@ class BlogController extends BaseController
|
|||||||
return $this->success(['data' => $blog->toArray()]);
|
return $this->success(['data' => $blog->toArray()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Delete(
|
||||||
|
path: '/api/v1/blog/{uuid}',
|
||||||
|
summary: 'Delete a blog post (admin only)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Blog post deleted',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden — admin role required'),
|
||||||
|
new OA\Response(response: 404, description: 'Blog post not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/blog/{uuid}', methods: ['DELETE'])]
|
#[Route('/api/v1/blog/{uuid}', methods: ['DELETE'])]
|
||||||
public function delete(string $uuid): JsonResponse
|
public function delete(string $uuid): JsonResponse
|
||||||
@@ -112,6 +312,64 @@ class BlogController extends BaseController
|
|||||||
|
|
||||||
// ── Image upload ──────────────────────────────────────────────────────────
|
// ── Image upload ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/file/upload/clinic_pro/blog/field_image',
|
||||||
|
summary: 'Upload a blog post image (admin only)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\MediaType(
|
||||||
|
mediaType: 'multipart/form-data',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
required: ['file'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'file', type: 'string', format: 'binary'),
|
||||||
|
new OA\Property(property: 'blog_uuid', type: 'string', format: 'uuid', nullable: true),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Image uploaded',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'image_url', type: 'string'),
|
||||||
|
new OA\Property(property: 'filename', type: 'string'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden — admin role required'),
|
||||||
|
new OA\Response(
|
||||||
|
response: 422,
|
||||||
|
description: 'No file provided or invalid file',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'errors',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/file/upload/clinic_pro/blog/field_image', methods: ['POST'])]
|
#[Route('/file/upload/clinic_pro/blog/field_image', methods: ['POST'])]
|
||||||
public function uploadImage(Request $request): JsonResponse
|
public function uploadImage(Request $request): JsonResponse
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use App\Doctor\Repository\DoctorRepository;
|
|||||||
use App\Shared\Constant\ErrorCodes;
|
use App\Shared\Constant\ErrorCodes;
|
||||||
use App\Shared\Controller\BaseController;
|
use App\Shared\Controller\BaseController;
|
||||||
use App\Shared\Service\FileValidatorService;
|
use App\Shared\Service\FileValidatorService;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
@@ -19,6 +20,7 @@ use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
|||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
use Symfony\Component\Uid\Uuid;
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
|
#[OA\Tag(name: 'Clinics')]
|
||||||
class ClinicController extends BaseController
|
class ClinicController extends BaseController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -30,6 +32,47 @@ class ClinicController extends BaseController
|
|||||||
private readonly string $projectDir,
|
private readonly string $projectDir,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/clinic',
|
||||||
|
summary: 'Create a new clinic',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'name', type: 'string'),
|
||||||
|
new OA\Property(property: 'info', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'address', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'telephone', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'working_days', type: 'array', items: new OA\Items(type: 'string'), nullable: true),
|
||||||
|
new OA\Property(property: '24_7', type: 'boolean', nullable: true),
|
||||||
|
new OA\Property(property: 'latitude', type: 'number', format: 'float', nullable: true),
|
||||||
|
new OA\Property(property: 'longitude', type: 'number', format: 'float', nullable: true),
|
||||||
|
new OA\Property(property: 'state', type: 'array', items: new OA\Items(type: 'integer'), nullable: true),
|
||||||
|
new OA\Property(property: 'city', type: 'array', items: new OA\Items(type: 'integer'), nullable: true),
|
||||||
|
new OA\Property(property: 'image_clinic', type: 'array', items: new OA\Items(type: 'string'), nullable: true),
|
||||||
|
new OA\Property(property: 'clinic_logo', type: 'array', items: new OA\Items(type: 'string'), nullable: true),
|
||||||
|
new OA\Property(property: 'doctors', type: 'array', items: new OA\Items(type: 'integer'), nullable: true),
|
||||||
|
new OA\Property(property: 'specialties', type: 'array', items: new OA\Items(type: 'integer'), nullable: true),
|
||||||
|
new OA\Property(property: 'doctor_services', type: 'array', items: new OA\Items(type: 'integer'), nullable: true),
|
||||||
|
new OA\Property(property: 'insurance', type: 'array', items: new OA\Items(type: 'integer'), nullable: true),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 201,
|
||||||
|
description: 'Clinic created',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/clinic', methods: ['POST'])]
|
#[Route('/api/v1/clinic', methods: ['POST'])]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
public function create(Request $request, #[CurrentUser] User $user): JsonResponse
|
public function create(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -50,6 +93,31 @@ class ClinicController extends BaseController
|
|||||||
return $this->success(['data' => $clinic->toDetailArray()], 201);
|
return $this->success(['data' => $clinic->toDetailArray()], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/clinic/{uuid}',
|
||||||
|
summary: 'Get clinic details by UUID',
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Clinic details',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 404, description: 'Clinic not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/clinic/{uuid}', methods: ['GET'])]
|
#[Route('/api/v1/clinic/{uuid}', methods: ['GET'])]
|
||||||
public function show(string $uuid): JsonResponse
|
public function show(string $uuid): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -63,6 +131,57 @@ class ClinicController extends BaseController
|
|||||||
return $this->success(['data' => $clinic->toDetailArray($stateData, $cityData)]);
|
return $this->success(['data' => $clinic->toDetailArray($stateData, $cityData)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Patch(
|
||||||
|
path: '/api/v1/clinic/{uuid}',
|
||||||
|
summary: 'Update clinic details',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'name', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'info', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'address', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'telephone', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'working_days', type: 'array', items: new OA\Items(type: 'string'), nullable: true),
|
||||||
|
new OA\Property(property: '24_7', type: 'boolean', nullable: true),
|
||||||
|
new OA\Property(property: 'latitude', type: 'number', format: 'float', nullable: true),
|
||||||
|
new OA\Property(property: 'longitude', type: 'number', format: 'float', nullable: true),
|
||||||
|
new OA\Property(property: 'state', type: 'array', items: new OA\Items(type: 'integer'), nullable: true),
|
||||||
|
new OA\Property(property: 'city', type: 'array', items: new OA\Items(type: 'integer'), nullable: true),
|
||||||
|
new OA\Property(property: 'image_clinic', type: 'array', items: new OA\Items(type: 'string'), nullable: true),
|
||||||
|
new OA\Property(property: 'clinic_logo', type: 'array', items: new OA\Items(type: 'string'), nullable: true),
|
||||||
|
new OA\Property(property: 'doctors', type: 'array', items: new OA\Items(type: 'integer'), nullable: true),
|
||||||
|
new OA\Property(property: 'specialties', type: 'array', items: new OA\Items(type: 'integer'), nullable: true),
|
||||||
|
new OA\Property(property: 'doctor_services', type: 'array', items: new OA\Items(type: 'integer'), nullable: true),
|
||||||
|
new OA\Property(property: 'insurance', type: 'array', items: new OA\Items(type: 'integer'), nullable: true),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Clinic updated',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden'),
|
||||||
|
new OA\Response(response: 404, description: 'Clinic not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/clinic/{uuid}', methods: ['PATCH'])]
|
#[Route('/api/v1/clinic/{uuid}', methods: ['PATCH'])]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
public function update(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
public function update(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -85,6 +204,37 @@ class ClinicController extends BaseController
|
|||||||
return $this->success(['data' => $clinic->toDetailArray($stateData, $cityData)]);
|
return $this->success(['data' => $clinic->toDetailArray($stateData, $cityData)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/clinics',
|
||||||
|
summary: 'List clinics with optional filters (paginated)',
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
|
||||||
|
new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 20)),
|
||||||
|
new OA\Parameter(name: 'name', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||||
|
new OA\Parameter(name: 'city', in: 'query', required: false, schema: new OA\Schema(type: 'integer')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Paginated list of clinics',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(type: 'object')),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'meta',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'totalRecords', type: 'integer'),
|
||||||
|
new OA\Property(property: 'totalPages', type: 'integer'),
|
||||||
|
new OA\Property(property: 'currentPage', type: 'integer'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/clinics', methods: ['GET'])]
|
#[Route('/api/v1/clinics', methods: ['GET'])]
|
||||||
public function list(Request $request): JsonResponse
|
public function list(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -99,6 +249,37 @@ class ClinicController extends BaseController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/clinic/doctor-list/{clinicUuid}',
|
||||||
|
summary: 'Get list of doctors for a clinic',
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'clinicUuid',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'List of doctors',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(type: 'object')),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 404, description: 'Clinic not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/clinic/doctor-list/{clinicUuid}', methods: ['GET'])]
|
#[Route('/api/v1/clinic/doctor-list/{clinicUuid}', methods: ['GET'])]
|
||||||
public function doctorList(string $clinicUuid): JsonResponse
|
public function doctorList(string $clinicUuid): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -115,6 +296,43 @@ class ClinicController extends BaseController
|
|||||||
return $this->success(['data' => $doctors]);
|
return $this->success(['data' => $doctors]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/file/upload/clinic_pro/clinic/field_image_clinic',
|
||||||
|
summary: 'Upload a clinic gallery image',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\MediaType(
|
||||||
|
mediaType: 'application/octet-stream',
|
||||||
|
schema: new OA\Schema(type: 'string', format: 'binary')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Image uploaded',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'fid', type: 'integer'),
|
||||||
|
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||||
|
new OA\Property(property: 'url', type: 'string'),
|
||||||
|
new OA\Property(property: 'filename', type: 'string'),
|
||||||
|
new OA\Property(property: 'filemime', type: 'string'),
|
||||||
|
new OA\Property(property: 'filesize', type: 'integer'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 422, description: 'Invalid file'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/file/upload/clinic_pro/clinic/field_image_clinic', methods: ['POST'])]
|
#[Route('/file/upload/clinic_pro/clinic/field_image_clinic', methods: ['POST'])]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
public function uploadImage(Request $request): JsonResponse
|
public function uploadImage(Request $request): JsonResponse
|
||||||
@@ -122,6 +340,43 @@ class ClinicController extends BaseController
|
|||||||
return $this->handleFileUpload($request, 'clinics/gallery');
|
return $this->handleFileUpload($request, 'clinics/gallery');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/file/upload/clinic_pro/clinic/field_clinic_logo',
|
||||||
|
summary: 'Upload a clinic logo image',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\MediaType(
|
||||||
|
mediaType: 'application/octet-stream',
|
||||||
|
schema: new OA\Schema(type: 'string', format: 'binary')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Logo uploaded',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'fid', type: 'integer'),
|
||||||
|
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||||
|
new OA\Property(property: 'url', type: 'string'),
|
||||||
|
new OA\Property(property: 'filename', type: 'string'),
|
||||||
|
new OA\Property(property: 'filemime', type: 'string'),
|
||||||
|
new OA\Property(property: 'filesize', type: 'integer'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 422, description: 'Invalid file'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/file/upload/clinic_pro/clinic/field_clinic_logo', methods: ['POST'])]
|
#[Route('/file/upload/clinic_pro/clinic/field_clinic_logo', methods: ['POST'])]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
public function uploadLogo(Request $request): JsonResponse
|
public function uploadLogo(Request $request): JsonResponse
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ use App\Doctor\Repository\DoctorRepository;
|
|||||||
use App\Shared\Constant\ErrorCodes;
|
use App\Shared\Constant\ErrorCodes;
|
||||||
use App\Shared\Controller\BaseController;
|
use App\Shared\Controller\BaseController;
|
||||||
use App\Shared\Service\FileValidatorService;
|
use App\Shared\Service\FileValidatorService;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[OA\Tag(name: 'Doctors')]
|
||||||
class DoctorController extends BaseController
|
class DoctorController extends BaseController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -31,6 +33,51 @@ class DoctorController extends BaseController
|
|||||||
|
|
||||||
// ── Doctor CRUD ───────────────────────────────────────────────────────────
|
// ── Doctor CRUD ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/doctor',
|
||||||
|
summary: 'Create a new doctor profile',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['title'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'title', type: 'string', description: 'Doctor name (also accepted as "name")'),
|
||||||
|
new OA\Property(property: 'gender', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'medical_system_code', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'degree', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'info', type: 'string', nullable: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'specialties',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(type: 'integer'),
|
||||||
|
nullable: true,
|
||||||
|
),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'doctor_services',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(type: 'integer'),
|
||||||
|
nullable: true,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 201,
|
||||||
|
description: 'Doctor created successfully',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object', description: 'Doctor detail object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 409, description: 'Doctor profile already exists'),
|
||||||
|
new OA\Response(response: 422, description: 'Validation error'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/doctor', methods: ['POST'])]
|
#[Route('/api/v1/doctor', methods: ['POST'])]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
public function create(Request $request, #[CurrentUser] User $user): JsonResponse
|
public function create(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -61,6 +108,26 @@ class DoctorController extends BaseController
|
|||||||
return $this->success(['data' => $doctor->toDetailArray()], 201);
|
return $this->success(['data' => $doctor->toDetailArray()], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/doctor/{uuid}',
|
||||||
|
summary: 'Get a doctor by UUID',
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Doctor detail',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object', description: 'Doctor detail object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 404, description: 'Doctor not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/doctor/{uuid}', methods: ['GET'])]
|
#[Route('/api/v1/doctor/{uuid}', methods: ['GET'])]
|
||||||
public function show(string $uuid): JsonResponse
|
public function show(string $uuid): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -72,6 +139,39 @@ class DoctorController extends BaseController
|
|||||||
return $this->success(['data' => $doctor->toDetailArray()]);
|
return $this->success(['data' => $doctor->toDetailArray()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/doctors',
|
||||||
|
summary: 'List doctors with optional filters (paginated)',
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
|
||||||
|
new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 20)),
|
||||||
|
new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
|
||||||
|
new OA\Parameter(name: 'specialty_id', in: 'query', required: false, schema: new OA\Schema(type: 'integer')),
|
||||||
|
new OA\Parameter(name: 'city_id', in: 'query', required: false, schema: new OA\Schema(type: 'integer')),
|
||||||
|
new OA\Parameter(name: 'state_id', in: 'query', required: false, schema: new OA\Schema(type: 'integer')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Paginated doctor list',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(type: 'object')),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'meta',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'totalRecords', type: 'integer'),
|
||||||
|
new OA\Property(property: 'totalPages', type: 'integer'),
|
||||||
|
new OA\Property(property: 'currentPage', type: 'integer'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/doctors', methods: ['GET'])]
|
#[Route('/api/v1/doctors', methods: ['GET'])]
|
||||||
public function list(Request $request): JsonResponse
|
public function list(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -86,6 +186,53 @@ class DoctorController extends BaseController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Patch(
|
||||||
|
path: '/api/v1/doctor/{uuid}',
|
||||||
|
summary: 'Update a doctor profile',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: false,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'title', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'gender', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'medical_system_code', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'degree', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'info', type: 'string', nullable: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'specialties',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(type: 'integer'),
|
||||||
|
nullable: true,
|
||||||
|
),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'doctor_services',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(type: 'integer'),
|
||||||
|
nullable: true,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Doctor updated successfully',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object', description: 'Doctor detail object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden'),
|
||||||
|
new OA\Response(response: 404, description: 'Doctor not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/doctor/{uuid}', methods: ['PATCH'])]
|
#[Route('/api/v1/doctor/{uuid}', methods: ['PATCH'])]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
public function update(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
public function update(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -108,6 +255,31 @@ class DoctorController extends BaseController
|
|||||||
return $this->success(['data' => $doctor->toDetailArray()]);
|
return $this->success(['data' => $doctor->toDetailArray()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Delete(
|
||||||
|
path: '/api/v1/doctor/{uuid}',
|
||||||
|
summary: 'Delete a doctor (ROLE_ADMIN only)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Doctor deleted successfully',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object', properties: [
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden — ROLE_ADMIN required'),
|
||||||
|
new OA\Response(response: 404, description: 'Doctor not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/doctor/{uuid}', methods: ['DELETE'])]
|
#[Route('/api/v1/doctor/{uuid}', methods: ['DELETE'])]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
public function delete(string $uuid): JsonResponse
|
public function delete(string $uuid): JsonResponse
|
||||||
@@ -123,6 +295,52 @@ class DoctorController extends BaseController
|
|||||||
|
|
||||||
// ── File Upload ───────────────────────────────────────────────────────────
|
// ── File Upload ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/file/upload/clinic_pro/doctor/field_image',
|
||||||
|
summary: 'Upload a doctor profile image (raw binary)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\MediaType(
|
||||||
|
mediaType: 'application/octet-stream',
|
||||||
|
schema: new OA\Schema(type: 'string', format: 'binary')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'Content-Disposition',
|
||||||
|
in: 'header',
|
||||||
|
required: true,
|
||||||
|
description: 'Must include filename, e.g. attachment; filename="photo.jpg"',
|
||||||
|
schema: new OA\Schema(type: 'string')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'File uploaded successfully',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'fid', type: 'integer'),
|
||||||
|
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||||
|
new OA\Property(property: 'url', type: 'string'),
|
||||||
|
new OA\Property(property: 'filename', type: 'string'),
|
||||||
|
new OA\Property(property: 'filemime', type: 'string'),
|
||||||
|
new OA\Property(property: 'filesize', type: 'integer'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 422, description: 'Invalid file'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/file/upload/clinic_pro/doctor/field_image', methods: ['POST'])]
|
#[Route('/file/upload/clinic_pro/doctor/field_image', methods: ['POST'])]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
public function uploadImage(Request $request): JsonResponse
|
public function uploadImage(Request $request): JsonResponse
|
||||||
@@ -171,6 +389,39 @@ class DoctorController extends BaseController
|
|||||||
|
|
||||||
// ── Doctor Addresses ──────────────────────────────────────────────────────
|
// ── Doctor Addresses ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/clinic-pro/doctor-address',
|
||||||
|
summary: 'Create a new doctor address',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: false,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'name', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'address', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'telephone', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'latitude', type: 'number', format: 'float', nullable: true),
|
||||||
|
new OA\Property(property: 'longitude', type: 'number', format: 'float', nullable: true),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 201,
|
||||||
|
description: 'Address created successfully',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object', description: 'Address object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden'),
|
||||||
|
new OA\Response(response: 404, description: 'Doctor not found'),
|
||||||
|
new OA\Response(response: 422, description: 'Validation error'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/clinic-pro/doctor-address', methods: ['POST'])]
|
#[Route('/api/v1/clinic-pro/doctor-address', methods: ['POST'])]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
public function createAddress(Request $request, #[CurrentUser] User $user): JsonResponse
|
public function createAddress(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -201,6 +452,28 @@ class DoctorController extends BaseController
|
|||||||
return $this->success(['data' => $address->toArray()], 201);
|
return $this->success(['data' => $address->toArray()], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/clinic-pro/doctor-address/{id}',
|
||||||
|
summary: 'Get a doctor address by ID',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Address detail',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object', description: 'Address object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 404, description: 'Address not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/clinic-pro/doctor-address/{id}', methods: ['GET'])]
|
#[Route('/api/v1/clinic-pro/doctor-address/{id}', methods: ['GET'])]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
public function showAddress(int $id): JsonResponse
|
public function showAddress(int $id): JsonResponse
|
||||||
@@ -213,6 +486,41 @@ class DoctorController extends BaseController
|
|||||||
return $this->success(['data' => $address->toArray()]);
|
return $this->success(['data' => $address->toArray()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Patch(
|
||||||
|
path: '/api/v1/clinic-pro/doctor-address/{id}',
|
||||||
|
summary: 'Update a doctor address',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: false,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'name', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'address', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'telephone', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'latitude', type: 'number', format: 'float', nullable: true),
|
||||||
|
new OA\Property(property: 'longitude', type: 'number', format: 'float', nullable: true),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Address updated successfully',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object', description: 'Address object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden'),
|
||||||
|
new OA\Response(response: 404, description: 'Address not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/clinic-pro/doctor-address/{id}', methods: ['PATCH'])]
|
#[Route('/api/v1/clinic-pro/doctor-address/{id}', methods: ['PATCH'])]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
public function updateAddress(int $id, Request $request, #[CurrentUser] User $user): JsonResponse
|
public function updateAddress(int $id, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -233,6 +541,31 @@ class DoctorController extends BaseController
|
|||||||
return $this->success(['data' => $address->toArray()]);
|
return $this->success(['data' => $address->toArray()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Delete(
|
||||||
|
path: '/api/v1/clinic-pro/doctor-address/{id}',
|
||||||
|
summary: 'Delete a doctor address',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Address deleted successfully',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object', properties: [
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden'),
|
||||||
|
new OA\Response(response: 404, description: 'Address not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/clinic-pro/doctor-address/{id}', methods: ['DELETE'])]
|
#[Route('/api/v1/clinic-pro/doctor-address/{id}', methods: ['DELETE'])]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
public function deleteAddress(int $id, #[CurrentUser] User $user): JsonResponse
|
public function deleteAddress(int $id, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -250,6 +583,26 @@ class DoctorController extends BaseController
|
|||||||
return $this->success(['message' => 'آدرس با موفقیت حذف شد']);
|
return $this->success(['message' => 'آدرس با موفقیت حذف شد']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/clinic-pro/doctor-addresses/{doctorId}',
|
||||||
|
summary: 'List all addresses for a doctor',
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'doctorId', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Array of address objects',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(type: 'object')),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 404, description: 'Doctor not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/clinic-pro/doctor-addresses/{doctorId}', methods: ['GET'])]
|
#[Route('/api/v1/clinic-pro/doctor-addresses/{doctorId}', methods: ['GET'])]
|
||||||
public function listAddresses(int $doctorId): JsonResponse
|
public function listAddresses(int $doctorId): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -263,15 +616,6 @@ class DoctorController extends BaseController
|
|||||||
return $this->success(['data' => $addresses]);
|
return $this->success(['data' => $addresses]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Clinic/Doctor list (stub — implemented fully in Task 06) ──────────────
|
|
||||||
|
|
||||||
#[Route('/api/v1/clinic/doctor-list/{clinicUuid}', methods: ['GET'])]
|
|
||||||
public function clinicDoctorList(string $clinicUuid): JsonResponse
|
|
||||||
{
|
|
||||||
// Full implementation in Task 06 (Clinic entity not yet created)
|
|
||||||
return $this->success(['data' => []]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private function hydrateDoctor(Doctor $doctor, array $data): void
|
private function hydrateDoctor(Doctor $doctor, array $data): void
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use App\Payment\Repository\PaymentRepository;
|
|||||||
use App\Payment\Service\CircuitBreakerService;
|
use App\Payment\Service\CircuitBreakerService;
|
||||||
use App\Shared\Constant\ErrorCodes;
|
use App\Shared\Constant\ErrorCodes;
|
||||||
use App\Shared\Controller\BaseController;
|
use App\Shared\Controller\BaseController;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -19,6 +20,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[OA\Tag(name: 'Payments')]
|
||||||
class PaymentController extends BaseController
|
class PaymentController extends BaseController
|
||||||
{
|
{
|
||||||
// Shaparak payment network callback IP ranges
|
// Shaparak payment network callback IP ranges
|
||||||
@@ -39,6 +41,99 @@ class PaymentController extends BaseController
|
|||||||
|
|
||||||
// ── Appointment Payment ───────────────────────────────────────────────────
|
// ── Appointment Payment ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/payment/appointment',
|
||||||
|
summary: 'Initiate an appointment payment',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['appointment_uuid', 'gateway'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'appointment_uuid', type: 'string', format: 'uuid'),
|
||||||
|
new OA\Property(property: 'gateway', type: 'string', enum: ['mellat', 'sep']),
|
||||||
|
new OA\Property(property: 'frontend_address', type: 'string', format: 'uri', nullable: true),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Payment initiated',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'payment_uuid', type: 'string', format: 'uuid'),
|
||||||
|
new OA\Property(property: 'redirect_url', type: 'string', format: 'uri'),
|
||||||
|
new OA\Property(property: 'order_id', type: 'string'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 401,
|
||||||
|
description: 'Unauthorized',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'errors',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 422,
|
||||||
|
description: 'Validation error',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'errors',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 503,
|
||||||
|
description: 'Gateway unavailable',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'errors',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
#[Route('/api/v1/payment/appointment', methods: ['POST'])]
|
#[Route('/api/v1/payment/appointment', methods: ['POST'])]
|
||||||
public function initiateAppointment(Request $request, #[CurrentUser] User $user): JsonResponse
|
public function initiateAppointment(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -100,6 +195,33 @@ class PaymentController extends BaseController
|
|||||||
|
|
||||||
// ── Payment Callback (public — no JWT) ───────────────────────────────────
|
// ── Payment Callback (public — no JWT) ───────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/payment/callback/{gateway}',
|
||||||
|
summary: 'Payment gateway callback (public, IP-restricted)',
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'gateway',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string', enum: ['mellat', 'sep'])
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Callback processed — either a redirect or JSON result',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean'),
|
||||||
|
new OA\Property(property: 'payment', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 302, description: 'Redirect to frontend with payment result'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden — IP not in allowed Shaparak ranges'),
|
||||||
|
new OA\Response(response: 404, description: 'Payment not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/payment/callback/{gateway}', methods: ['POST', 'GET'])]
|
#[Route('/api/v1/payment/callback/{gateway}', methods: ['POST', 'GET'])]
|
||||||
public function callback(string $gateway, Request $request): \Symfony\Component\HttpFoundation\Response
|
public function callback(string $gateway, Request $request): \Symfony\Component\HttpFoundation\Response
|
||||||
{
|
{
|
||||||
@@ -139,6 +261,99 @@ class PaymentController extends BaseController
|
|||||||
|
|
||||||
// ── Subscription Payment ──────────────────────────────────────────────────
|
// ── Subscription Payment ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/subscription-payment',
|
||||||
|
summary: 'Initiate a subscription payment',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['gateway', 'amount_rials'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'gateway', type: 'string', enum: ['mellat', 'sep']),
|
||||||
|
new OA\Property(property: 'frontend_address', type: 'string', format: 'uri', nullable: true),
|
||||||
|
new OA\Property(property: 'amount_rials', type: 'integer', minimum: 1),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Subscription payment initiated',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'payment_uuid', type: 'string', format: 'uuid'),
|
||||||
|
new OA\Property(property: 'redirect_url', type: 'string', format: 'uri'),
|
||||||
|
new OA\Property(property: 'order_id', type: 'string'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 401,
|
||||||
|
description: 'Unauthorized',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'errors',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 422,
|
||||||
|
description: 'Validation error',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'errors',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 503,
|
||||||
|
description: 'Gateway unavailable',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'errors',
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
#[Route('/api/v1/subscription-payment', methods: ['POST'])]
|
#[Route('/api/v1/subscription-payment', methods: ['POST'])]
|
||||||
public function initiateSubscription(Request $request, #[CurrentUser] User $user): JsonResponse
|
public function initiateSubscription(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -187,6 +402,33 @@ class PaymentController extends BaseController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/subscription-payment/callback/{gateway}',
|
||||||
|
summary: 'Subscription payment gateway callback (public, IP-restricted)',
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'gateway',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string', enum: ['mellat', 'sep'])
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Callback processed — either a redirect or JSON result',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean'),
|
||||||
|
new OA\Property(property: 'payment', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 302, description: 'Redirect to frontend with payment result'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden — IP not in allowed Shaparak ranges'),
|
||||||
|
new OA\Response(response: 404, description: 'Payment not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/subscription-payment/callback/{gateway}', methods: ['POST', 'GET'])]
|
#[Route('/api/v1/subscription-payment/callback/{gateway}', methods: ['POST', 'GET'])]
|
||||||
public function subscriptionCallback(string $gateway, Request $request): \Symfony\Component\HttpFoundation\Response
|
public function subscriptionCallback(string $gateway, Request $request): \Symfony\Component\HttpFoundation\Response
|
||||||
{
|
{
|
||||||
@@ -195,6 +437,51 @@ class PaymentController extends BaseController
|
|||||||
|
|
||||||
// ── Status ────────────────────────────────────────────────────────────────
|
// ── Status ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/payment/{uuid}',
|
||||||
|
summary: 'Get payment status by UUID',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Payment details',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||||
|
new OA\Property(property: 'status', type: 'string'),
|
||||||
|
new OA\Property(property: 'amount_rials', type: 'integer'),
|
||||||
|
new OA\Property(property: 'gateway', type: 'string'),
|
||||||
|
new OA\Property(property: 'reference_id', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden'),
|
||||||
|
new OA\Response(response: 404, description: 'Payment not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
#[Route('/api/v1/payment/{uuid}', methods: ['GET'])]
|
#[Route('/api/v1/payment/{uuid}', methods: ['GET'])]
|
||||||
public function getStatus(string $uuid, #[CurrentUser] User $user): JsonResponse
|
public function getStatus(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ use App\Rating\Repository\LikeRepository;
|
|||||||
use App\Rating\Repository\RateRepository;
|
use App\Rating\Repository\RateRepository;
|
||||||
use App\Shared\Constant\ErrorCodes;
|
use App\Shared\Constant\ErrorCodes;
|
||||||
use App\Shared\Controller\BaseController;
|
use App\Shared\Controller\BaseController;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[OA\Tag(name: 'Ratings & Comments')]
|
||||||
class RatingController extends BaseController
|
class RatingController extends BaseController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -29,6 +31,36 @@ class RatingController extends BaseController
|
|||||||
|
|
||||||
// ── Ratings ───────────────────────────────────────────────────────────────
|
// ── Ratings ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/rate',
|
||||||
|
summary: 'Submit or update a rating for a doctor',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['doctor_uuid', 'score'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'doctor_uuid', type: 'string', description: 'UUID of the doctor'),
|
||||||
|
new OA\Property(property: 'score', type: 'integer', minimum: 1, maximum: 5, description: 'Rating score (1–5)'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 201,
|
||||||
|
description: 'Rate object created',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object', description: 'Rate object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 404, description: 'Doctor not found'),
|
||||||
|
new OA\Response(response: 422, description: 'Validation error — score out of range'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
#[Route('/api/v1/rate', methods: ['POST'])]
|
#[Route('/api/v1/rate', methods: ['POST'])]
|
||||||
public function rate(Request $request, #[CurrentUser] User $user): JsonResponse
|
public function rate(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -59,6 +91,32 @@ class RatingController extends BaseController
|
|||||||
return $this->success(['data' => $rate->toArray()], 201);
|
return $this->success(['data' => $rate->toArray()], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/rate/{doctorUuid}',
|
||||||
|
summary: 'Get average rating for a doctor',
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'doctorUuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Average rating',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'average', type: 'number', format: 'float'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 404, description: 'Doctor not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/rate/{doctorUuid}', methods: ['GET'])]
|
#[Route('/api/v1/rate/{doctorUuid}', methods: ['GET'])]
|
||||||
public function getAverage(string $doctorUuid): JsonResponse
|
public function getAverage(string $doctorUuid): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -72,6 +130,36 @@ class RatingController extends BaseController
|
|||||||
|
|
||||||
// ── Comments ──────────────────────────────────────────────────────────────
|
// ── Comments ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/comment',
|
||||||
|
summary: 'Submit a comment for a doctor',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['doctor_uuid', 'body'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'doctor_uuid', type: 'string', description: 'UUID of the doctor'),
|
||||||
|
new OA\Property(property: 'body', type: 'string', description: 'Comment text'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 201,
|
||||||
|
description: 'Comment created',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object', description: 'Comment object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 404, description: 'Doctor not found'),
|
||||||
|
new OA\Response(response: 422, description: 'Validation error'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
#[Route('/api/v1/comment', methods: ['POST'])]
|
#[Route('/api/v1/comment', methods: ['POST'])]
|
||||||
public function createComment(Request $request, #[CurrentUser] User $user): JsonResponse
|
public function createComment(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -95,6 +183,26 @@ class RatingController extends BaseController
|
|||||||
return $this->success(['data' => $comment->toArray()], 201);
|
return $this->success(['data' => $comment->toArray()], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/comments/{doctorUuid}',
|
||||||
|
summary: 'List approved comments for a doctor',
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'doctorUuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Array of comment objects',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(type: 'object')),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 404, description: 'Doctor not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/comments/{doctorUuid}', methods: ['GET'])]
|
#[Route('/api/v1/comments/{doctorUuid}', methods: ['GET'])]
|
||||||
public function listComments(string $doctorUuid): JsonResponse
|
public function listComments(string $doctorUuid): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -111,6 +219,31 @@ class RatingController extends BaseController
|
|||||||
return $this->success(['data' => $comments]);
|
return $this->success(['data' => $comments]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Delete(
|
||||||
|
path: '/api/v1/comment/{uuid}',
|
||||||
|
summary: 'Delete a comment (owner or ROLE_ADMIN)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Comment deleted',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object', properties: [
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden'),
|
||||||
|
new OA\Response(response: 404, description: 'Comment not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
#[Route('/api/v1/comment/{uuid}', methods: ['DELETE'])]
|
#[Route('/api/v1/comment/{uuid}', methods: ['DELETE'])]
|
||||||
public function deleteComment(string $uuid, #[CurrentUser] User $user): JsonResponse
|
public function deleteComment(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||||
@@ -130,6 +263,25 @@ class RatingController extends BaseController
|
|||||||
|
|
||||||
// ── Admin: comment moderation ─────────────────────────────────────────────
|
// ── Admin: comment moderation ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/admin/comments/pending',
|
||||||
|
summary: 'List all pending comments (ROLE_ADMIN only)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Array of pending comment objects',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(type: 'object')),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden — ROLE_ADMIN required'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/admin/comments/pending', methods: ['GET'])]
|
#[Route('/api/v1/admin/comments/pending', methods: ['GET'])]
|
||||||
public function pendingComments(): JsonResponse
|
public function pendingComments(): JsonResponse
|
||||||
@@ -138,6 +290,29 @@ class RatingController extends BaseController
|
|||||||
return $this->success(['data' => $comments]);
|
return $this->success(['data' => $comments]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/admin/comment/{uuid}/approve',
|
||||||
|
summary: 'Approve a comment (ROLE_ADMIN only)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Comment approved',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object', description: 'Comment object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden — ROLE_ADMIN required'),
|
||||||
|
new OA\Response(response: 404, description: 'Comment not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/admin/comment/{uuid}/approve', methods: ['POST'])]
|
#[Route('/api/v1/admin/comment/{uuid}/approve', methods: ['POST'])]
|
||||||
public function approveComment(string $uuid): JsonResponse
|
public function approveComment(string $uuid): JsonResponse
|
||||||
@@ -151,6 +326,29 @@ class RatingController extends BaseController
|
|||||||
return $this->success(['data' => $comment->toArray()]);
|
return $this->success(['data' => $comment->toArray()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/admin/comment/{uuid}/reject',
|
||||||
|
summary: 'Reject a comment (ROLE_ADMIN only)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Comment rejected',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object', description: 'Comment object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden — ROLE_ADMIN required'),
|
||||||
|
new OA\Response(response: 404, description: 'Comment not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/admin/comment/{uuid}/reject', methods: ['POST'])]
|
#[Route('/api/v1/admin/comment/{uuid}/reject', methods: ['POST'])]
|
||||||
public function rejectComment(string $uuid): JsonResponse
|
public function rejectComment(string $uuid): JsonResponse
|
||||||
@@ -166,6 +364,52 @@ class RatingController extends BaseController
|
|||||||
|
|
||||||
// ── Likes (toggle) ────────────────────────────────────────────────────────
|
// ── Likes (toggle) ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/like/{commentUuid}',
|
||||||
|
summary: 'Toggle like on a comment',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'commentUuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Like removed (unliked)',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'liked', type: 'boolean', example: false),
|
||||||
|
new OA\Property(property: 'likes', type: 'integer'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 201,
|
||||||
|
description: 'Like added',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'data',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'liked', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'likes', type: 'integer'),
|
||||||
|
],
|
||||||
|
type: 'object'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 404, description: 'Comment not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
#[Route('/api/v1/like/{commentUuid}', methods: ['POST'])]
|
#[Route('/api/v1/like/{commentUuid}', methods: ['POST'])]
|
||||||
public function toggleLike(string $commentUuid, #[CurrentUser] User $user): JsonResponse
|
public function toggleLike(string $commentUuid, #[CurrentUser] User $user): JsonResponse
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ use App\Representation\Service\JalaliDateService;
|
|||||||
use App\Shared\Constant\ErrorCodes;
|
use App\Shared\Constant\ErrorCodes;
|
||||||
use App\Shared\Controller\BaseController;
|
use App\Shared\Controller\BaseController;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[OA\Tag(name: 'Representations')]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
class RepresentationController extends BaseController
|
class RepresentationController extends BaseController
|
||||||
{
|
{
|
||||||
@@ -28,6 +30,54 @@ class RepresentationController extends BaseController
|
|||||||
|
|
||||||
// ── CRUD ──────────────────────────────────────────────────────────────────
|
// ── CRUD ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/representation',
|
||||||
|
summary: 'Create a new representation (ROLE_ADMIN)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['full_name', 'mobile_number'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'full_name', type: 'string'),
|
||||||
|
new OA\Property(property: 'mobile_number', type: 'string'),
|
||||||
|
new OA\Property(property: 'city_id', type: 'integer', nullable: true),
|
||||||
|
new OA\Property(property: 'commission_percent', type: 'number', format: 'float', nullable: true),
|
||||||
|
new OA\Property(property: 'bank_account', type: 'object', nullable: true),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 201,
|
||||||
|
description: 'Representation created',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(
|
||||||
|
response: 409,
|
||||||
|
description: 'Conflict – user is already a representation',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||||
|
new OA\Property(property: 'errors', type: 'array', items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'code', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
new OA\Response(response: 422, description: 'Validation error'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/representation', methods: ['POST'])]
|
#[Route('/api/v1/representation', methods: ['POST'])]
|
||||||
public function create(Request $request): JsonResponse
|
public function create(Request $request): JsonResponse
|
||||||
@@ -62,6 +112,29 @@ class RepresentationController extends BaseController
|
|||||||
return $this->success(['data' => $rep->toArray()], 201);
|
return $this->success(['data' => $rep->toArray()], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/representation/{uuid}',
|
||||||
|
summary: 'Get a representation by UUID (own or admin)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Representation details',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden'),
|
||||||
|
new OA\Response(response: 404, description: 'Not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/representation/{uuid}', methods: ['GET'])]
|
#[Route('/api/v1/representation/{uuid}', methods: ['GET'])]
|
||||||
public function get(string $uuid, #[CurrentUser] User $user): JsonResponse
|
public function get(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -77,6 +150,41 @@ class RepresentationController extends BaseController
|
|||||||
return $this->success(['data' => $rep->toArray()]);
|
return $this->success(['data' => $rep->toArray()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Patch(
|
||||||
|
path: '/api/v1/representation/{uuid}',
|
||||||
|
summary: 'Update a representation (own or admin)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: false,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'full_name', type: 'string'),
|
||||||
|
new OA\Property(property: 'city_id', type: 'integer', nullable: true),
|
||||||
|
new OA\Property(property: 'bank_account', type: 'object', nullable: true),
|
||||||
|
new OA\Property(property: 'commission_percent', type: 'number', format: 'float'),
|
||||||
|
new OA\Property(property: 'active', type: 'boolean'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Representation updated',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden'),
|
||||||
|
new OA\Response(response: 404, description: 'Not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/representation/{uuid}', methods: ['PATCH'])]
|
#[Route('/api/v1/representation/{uuid}', methods: ['PATCH'])]
|
||||||
public function update(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
public function update(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -101,6 +209,31 @@ class RepresentationController extends BaseController
|
|||||||
return $this->success(['data' => $rep->toArray()]);
|
return $this->success(['data' => $rep->toArray()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Delete(
|
||||||
|
path: '/api/v1/representation/{uuid}',
|
||||||
|
summary: 'Delete a representation (ROLE_ADMIN)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Representation deleted',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', properties: [
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
new OA\Response(response: 404, description: 'Not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/representation/{uuid}', methods: ['DELETE'])]
|
#[Route('/api/v1/representation/{uuid}', methods: ['DELETE'])]
|
||||||
public function delete(string $uuid): JsonResponse
|
public function delete(string $uuid): JsonResponse
|
||||||
@@ -116,6 +249,41 @@ class RepresentationController extends BaseController
|
|||||||
|
|
||||||
// ── Dashboard: monthly stats ──────────────────────────────────────────────
|
// ── Dashboard: monthly stats ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/representation/{uuid}/dashboard/monthly',
|
||||||
|
summary: 'Get monthly dashboard stats for a representation',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
new OA\Parameter(name: 'year', in: 'query', required: false, description: 'Jalali year', schema: new OA\Schema(type: 'integer')),
|
||||||
|
new OA\Parameter(name: 'month', in: 'query', required: false, description: 'Jalali month (1–12)', schema: new OA\Schema(type: 'integer', minimum: 1, maximum: 12)),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Monthly statistics',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', properties: [
|
||||||
|
new OA\Property(property: 'period', properties: [
|
||||||
|
new OA\Property(property: 'jalali_year', type: 'integer'),
|
||||||
|
new OA\Property(property: 'jalali_month', type: 'integer'),
|
||||||
|
], type: 'object'),
|
||||||
|
new OA\Property(property: 'stats', properties: [
|
||||||
|
new OA\Property(property: 'total_payments', type: 'integer'),
|
||||||
|
new OA\Property(property: 'total_revenue_rials', type: 'integer'),
|
||||||
|
new OA\Property(property: 'total_appointments', type: 'integer'),
|
||||||
|
], type: 'object'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden'),
|
||||||
|
new OA\Response(response: 404, description: 'Not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/representation/{uuid}/dashboard/monthly', methods: ['GET'])]
|
#[Route('/api/v1/representation/{uuid}/dashboard/monthly', methods: ['GET'])]
|
||||||
public function dashboardMonthly(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
public function dashboardMonthly(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -139,6 +307,49 @@ class RepresentationController extends BaseController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/representation/{uuid}/dashboard/yearly',
|
||||||
|
summary: 'Get yearly dashboard stats for a representation',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
new OA\Parameter(name: 'year', in: 'query', required: false, description: 'Jalali year', schema: new OA\Schema(type: 'integer')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Yearly statistics broken down by month',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', properties: [
|
||||||
|
new OA\Property(property: 'period', properties: [
|
||||||
|
new OA\Property(property: 'jalali_year', type: 'integer'),
|
||||||
|
], type: 'object'),
|
||||||
|
new OA\Property(property: 'months', type: 'array', items: new OA\Items(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'jalali_month', type: 'integer'),
|
||||||
|
new OA\Property(property: 'stats', properties: [
|
||||||
|
new OA\Property(property: 'total_payments', type: 'integer'),
|
||||||
|
new OA\Property(property: 'total_revenue_rials', type: 'integer'),
|
||||||
|
new OA\Property(property: 'total_appointments', type: 'integer'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
new OA\Property(property: 'totals', properties: [
|
||||||
|
new OA\Property(property: 'total_payments', type: 'integer'),
|
||||||
|
new OA\Property(property: 'total_revenue_rials', type: 'integer'),
|
||||||
|
new OA\Property(property: 'total_appointments', type: 'integer'),
|
||||||
|
], type: 'object'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden'),
|
||||||
|
new OA\Response(response: 404, description: 'Not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/representation/{uuid}/dashboard/yearly', methods: ['GET'])]
|
#[Route('/api/v1/representation/{uuid}/dashboard/yearly', methods: ['GET'])]
|
||||||
public function dashboardYearly(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
public function dashboardYearly(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ use App\Settlement\Repository\SettlementRepository;
|
|||||||
use App\Settlement\Repository\WalletTransactionRepository;
|
use App\Settlement\Repository\WalletTransactionRepository;
|
||||||
use App\Shared\Constant\ErrorCodes;
|
use App\Shared\Constant\ErrorCodes;
|
||||||
use App\Shared\Controller\BaseController;
|
use App\Shared\Controller\BaseController;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[OA\Tag(name: 'Settlements')]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
class SettlementController extends BaseController
|
class SettlementController extends BaseController
|
||||||
{
|
{
|
||||||
@@ -25,6 +27,27 @@ class SettlementController extends BaseController
|
|||||||
|
|
||||||
// ── Wallet ────────────────────────────────────────────────────────────────
|
// ── Wallet ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/wallet/balance',
|
||||||
|
summary: 'Get current wallet balance and recent transactions',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Wallet balance and recent transactions',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', properties: [
|
||||||
|
new OA\Property(property: 'balance_rials', type: 'integer'),
|
||||||
|
new OA\Property(property: 'recent_transactions', type: 'array', items: new OA\Items(type: 'object')),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/wallet/balance', methods: ['GET'])]
|
#[Route('/api/v1/wallet/balance', methods: ['GET'])]
|
||||||
public function balance(#[CurrentUser] User $user): JsonResponse
|
public function balance(#[CurrentUser] User $user): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -40,6 +63,24 @@ class SettlementController extends BaseController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/wallet/transactions',
|
||||||
|
summary: 'List all wallet transactions for the authenticated user',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'List of wallet transactions',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(type: 'object')),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/wallet/transactions', methods: ['GET'])]
|
#[Route('/api/v1/wallet/transactions', methods: ['GET'])]
|
||||||
public function transactions(#[CurrentUser] User $user): JsonResponse
|
public function transactions(#[CurrentUser] User $user): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -53,6 +94,35 @@ class SettlementController extends BaseController
|
|||||||
|
|
||||||
// ── Settlement Requests ───────────────────────────────────────────────────
|
// ── Settlement Requests ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/settlement',
|
||||||
|
summary: 'Request a new settlement (withdrawal)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['amount_rials'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'amount_rials', type: 'integer', minimum: 1),
|
||||||
|
new OA\Property(property: 'bank_account', type: 'object', nullable: true),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 201,
|
||||||
|
description: 'Settlement request created',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 422, description: 'Validation error or insufficient balance'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/settlement', methods: ['POST'])]
|
#[Route('/api/v1/settlement', methods: ['POST'])]
|
||||||
public function request(Request $request, #[CurrentUser] User $user): JsonResponse
|
public function request(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -80,6 +150,24 @@ class SettlementController extends BaseController
|
|||||||
return $this->success(['data' => $settlement->toArray()], 201);
|
return $this->success(['data' => $settlement->toArray()], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/settlement',
|
||||||
|
summary: 'List settlement requests for the authenticated user',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'List of settlement requests',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(type: 'object')),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/settlement', methods: ['GET'])]
|
#[Route('/api/v1/settlement', methods: ['GET'])]
|
||||||
public function listMine(#[CurrentUser] User $user): JsonResponse
|
public function listMine(#[CurrentUser] User $user): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -91,6 +179,29 @@ class SettlementController extends BaseController
|
|||||||
return $this->success(['data' => $settlements]);
|
return $this->success(['data' => $settlements]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/settlement/{uuid}',
|
||||||
|
summary: 'Get a settlement request by UUID (own or admin)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Settlement request details',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden'),
|
||||||
|
new OA\Response(response: 404, description: 'Not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/settlement/{uuid}', methods: ['GET'])]
|
#[Route('/api/v1/settlement/{uuid}', methods: ['GET'])]
|
||||||
public function get(string $uuid, #[CurrentUser] User $user): JsonResponse
|
public function get(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -108,6 +219,38 @@ class SettlementController extends BaseController
|
|||||||
|
|
||||||
// ── Admin Actions ─────────────────────────────────────────────────────────
|
// ── Admin Actions ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/settlement/{uuid}/approve',
|
||||||
|
summary: 'Approve a pending settlement request (ROLE_ADMIN)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: false,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'note', type: 'string', nullable: true),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Settlement approved',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
new OA\Response(response: 404, description: 'Not found'),
|
||||||
|
new OA\Response(response: 422, description: 'Settlement is not in pending state'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/settlement/{uuid}/approve', methods: ['POST'])]
|
#[Route('/api/v1/settlement/{uuid}/approve', methods: ['POST'])]
|
||||||
public function approve(string $uuid, Request $request, #[CurrentUser] User $admin): JsonResponse
|
public function approve(string $uuid, Request $request, #[CurrentUser] User $admin): JsonResponse
|
||||||
@@ -128,6 +271,39 @@ class SettlementController extends BaseController
|
|||||||
return $this->success(['data' => $settlement->toArray()]);
|
return $this->success(['data' => $settlement->toArray()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/settlement/{uuid}/reject',
|
||||||
|
summary: 'Reject a pending settlement request (ROLE_ADMIN)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['note'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'note', type: 'string', description: 'Rejection reason (required)'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Settlement rejected and amount refunded to wallet',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
new OA\Response(response: 404, description: 'Not found'),
|
||||||
|
new OA\Response(response: 422, description: 'Settlement is not pending or note is missing'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/settlement/{uuid}/reject', methods: ['POST'])]
|
#[Route('/api/v1/settlement/{uuid}/reject', methods: ['POST'])]
|
||||||
public function reject(string $uuid, Request $request, #[CurrentUser] User $admin): JsonResponse
|
public function reject(string $uuid, Request $request, #[CurrentUser] User $admin): JsonResponse
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ class ExceptionSubscriber implements EventSubscriberInterface
|
|||||||
|
|
||||||
public function onKernelException(ExceptionEvent $event): void
|
public function onKernelException(ExceptionEvent $event): void
|
||||||
{
|
{
|
||||||
|
if (str_starts_with($event->getRequest()->getPathInfo(), '/api/doc')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$exception = $event->getThrowable();
|
$exception = $event->getThrowable();
|
||||||
|
|
||||||
if ($exception instanceof AppException) {
|
if ($exception instanceof AppException) {
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ class SecurityHeadersSubscriber implements EventSubscriberInterface
|
|||||||
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (str_starts_with($event->getRequest()->getPathInfo(), '/api')) {
|
$path = $event->getRequest()->getPathInfo();
|
||||||
|
if (str_starts_with($path, '/api') && !str_starts_with($path, '/api/doc')) {
|
||||||
$response->headers->set('Content-Security-Policy', "default-src 'none'");
|
$response->headers->set('Content-Security-Policy', "default-src 'none'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ use App\Shared\Controller\BaseController;
|
|||||||
use App\Sms\Entity\SmsTemplate;
|
use App\Sms\Entity\SmsTemplate;
|
||||||
use App\Sms\Repository\SmsTemplateRepository;
|
use App\Sms\Repository\SmsTemplateRepository;
|
||||||
use App\Sms\Service\SmsService;
|
use App\Sms\Service\SmsService;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[OA\Tag(name: 'SMS')]
|
||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
class SmsController extends BaseController
|
class SmsController extends BaseController
|
||||||
{
|
{
|
||||||
@@ -24,6 +26,39 @@ class SmsController extends BaseController
|
|||||||
|
|
||||||
// ── Send SMS directly ─────────────────────────────────────────────────────
|
// ── Send SMS directly ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/sms/send',
|
||||||
|
summary: 'Send an SMS message directly (ROLE_ADMIN)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['mobile', 'message'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'mobile', type: 'string'),
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
new OA\Property(property: 'provider', type: 'string', default: 'kavenegar'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'SMS queued for delivery',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', properties: [
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
new OA\Response(response: 422, description: 'Validation error'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/sms/send', methods: ['POST'])]
|
#[Route('/api/v1/sms/send', methods: ['POST'])]
|
||||||
public function send(Request $request): JsonResponse
|
public function send(Request $request): JsonResponse
|
||||||
@@ -44,6 +79,37 @@ class SmsController extends BaseController
|
|||||||
|
|
||||||
// ── Templates ─────────────────────────────────────────────────────────────
|
// ── Templates ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/sms/template',
|
||||||
|
summary: 'Create a new SMS template (ROLE_ADMIN)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['name', 'body'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'name', type: 'string'),
|
||||||
|
new OA\Property(property: 'body', type: 'string'),
|
||||||
|
new OA\Property(property: 'variables', type: 'array', items: new OA\Items(type: 'string')),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 201,
|
||||||
|
description: 'Template created',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
new OA\Response(response: 422, description: 'Validation error'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/sms/template', methods: ['POST'])]
|
#[Route('/api/v1/sms/template', methods: ['POST'])]
|
||||||
public function createTemplate(Request $request): JsonResponse
|
public function createTemplate(Request $request): JsonResponse
|
||||||
@@ -63,6 +129,28 @@ class SmsController extends BaseController
|
|||||||
return $this->success(['data' => $template->toArray()], 201);
|
return $this->success(['data' => $template->toArray()], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/sms/template/{uuid}',
|
||||||
|
summary: 'Get an SMS template by UUID',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Template details',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 404, description: 'Not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[Route('/api/v1/sms/template/{uuid}', methods: ['GET'])]
|
#[Route('/api/v1/sms/template/{uuid}', methods: ['GET'])]
|
||||||
public function getTemplate(string $uuid): JsonResponse
|
public function getTemplate(string $uuid): JsonResponse
|
||||||
{
|
{
|
||||||
@@ -73,6 +161,40 @@ class SmsController extends BaseController
|
|||||||
return $this->success(['data' => $template->toArray()]);
|
return $this->success(['data' => $template->toArray()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Patch(
|
||||||
|
path: '/api/v1/sms/template/{uuid}',
|
||||||
|
summary: 'Update an SMS template (ROLE_ADMIN, only non-approved templates)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: false,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'name', type: 'string'),
|
||||||
|
new OA\Property(property: 'body', type: 'string'),
|
||||||
|
new OA\Property(property: 'variables', type: 'array', items: new OA\Items(type: 'string')),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Template updated',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
new OA\Response(response: 404, description: 'Not found'),
|
||||||
|
new OA\Response(response: 422, description: 'Template is already approved and cannot be edited'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/sms/template/{uuid}', methods: ['PATCH'])]
|
#[Route('/api/v1/sms/template/{uuid}', methods: ['PATCH'])]
|
||||||
public function updateTemplate(string $uuid, Request $request): JsonResponse
|
public function updateTemplate(string $uuid, Request $request): JsonResponse
|
||||||
@@ -96,6 +218,29 @@ class SmsController extends BaseController
|
|||||||
return $this->success(['data' => $template->toArray()]);
|
return $this->success(['data' => $template->toArray()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/sms/template/{uuid}/submit',
|
||||||
|
summary: 'Submit an SMS template for review (ROLE_ADMIN)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Template submitted for review',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
new OA\Response(response: 404, description: 'Not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/sms/template/{uuid}/submit', methods: ['POST'])]
|
#[Route('/api/v1/sms/template/{uuid}/submit', methods: ['POST'])]
|
||||||
public function submitTemplate(string $uuid): JsonResponse
|
public function submitTemplate(string $uuid): JsonResponse
|
||||||
@@ -111,6 +256,31 @@ class SmsController extends BaseController
|
|||||||
return $this->success(['data' => $template->toArray()]);
|
return $this->success(['data' => $template->toArray()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Delete(
|
||||||
|
path: '/api/v1/sms/template/{uuid}',
|
||||||
|
summary: 'Delete an SMS template (ROLE_ADMIN)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Template deleted',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', properties: [
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
new OA\Response(response: 404, description: 'Not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/sms/template/{uuid}', methods: ['DELETE'])]
|
#[Route('/api/v1/sms/template/{uuid}', methods: ['DELETE'])]
|
||||||
public function deleteTemplate(string $uuid): JsonResponse
|
public function deleteTemplate(string $uuid): JsonResponse
|
||||||
@@ -125,6 +295,25 @@ class SmsController extends BaseController
|
|||||||
|
|
||||||
// ── Admin moderation ──────────────────────────────────────────────────────
|
// ── Admin moderation ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
path: '/api/v1/admin/sms/templates',
|
||||||
|
summary: 'List all SMS templates (ROLE_ADMIN)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'List of all SMS templates',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'array', items: new OA\Items(type: 'object')),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/admin/sms/templates', methods: ['GET'])]
|
#[Route('/api/v1/admin/sms/templates', methods: ['GET'])]
|
||||||
public function listTemplates(): JsonResponse
|
public function listTemplates(): JsonResponse
|
||||||
@@ -133,6 +322,38 @@ class SmsController extends BaseController
|
|||||||
return $this->success(['data' => $templates]);
|
return $this->success(['data' => $templates]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/admin/sms/template/{uuid}/approve',
|
||||||
|
summary: 'Approve an SMS template (ROLE_ADMIN)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: false,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'note', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'provider_code', type: 'string', nullable: true),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Template approved',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
new OA\Response(response: 404, description: 'Not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/admin/sms/template/{uuid}/approve', methods: ['POST'])]
|
#[Route('/api/v1/admin/sms/template/{uuid}/approve', methods: ['POST'])]
|
||||||
public function approveTemplate(string $uuid, Request $request): JsonResponse
|
public function approveTemplate(string $uuid, Request $request): JsonResponse
|
||||||
@@ -150,6 +371,39 @@ class SmsController extends BaseController
|
|||||||
return $this->success(['data' => $template->toArray()]);
|
return $this->success(['data' => $template->toArray()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/admin/sms/template/{uuid}/reject',
|
||||||
|
summary: 'Reject an SMS template (ROLE_ADMIN)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['note'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'note', type: 'string', description: 'Rejection reason (required)'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Template rejected',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
new OA\Response(response: 404, description: 'Not found'),
|
||||||
|
new OA\Response(response: 422, description: 'Rejection note is required'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/admin/sms/template/{uuid}/reject', methods: ['POST'])]
|
#[Route('/api/v1/admin/sms/template/{uuid}/reject', methods: ['POST'])]
|
||||||
public function rejectTemplate(string $uuid, Request $request): JsonResponse
|
public function rejectTemplate(string $uuid, Request $request): JsonResponse
|
||||||
@@ -172,6 +426,41 @@ class SmsController extends BaseController
|
|||||||
|
|
||||||
// ── Send via template ─────────────────────────────────────────────────────
|
// ── Send via template ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
path: '/api/v1/sms/send-template',
|
||||||
|
summary: 'Send an SMS using an approved template (ROLE_ADMIN)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
required: ['mobile', 'template_uuid'],
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'mobile', type: 'string'),
|
||||||
|
new OA\Property(property: 'template_uuid', type: 'string'),
|
||||||
|
new OA\Property(property: 'vars', type: 'object', description: 'Key-value map of template variables'),
|
||||||
|
new OA\Property(property: 'provider', type: 'string', default: 'kavenegar'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'SMS queued via template',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||||
|
new OA\Property(property: 'data', properties: [
|
||||||
|
new OA\Property(property: 'message', type: 'string'),
|
||||||
|
], type: 'object'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||||
|
new OA\Response(response: 403, description: 'Forbidden – ROLE_ADMIN required'),
|
||||||
|
new OA\Response(response: 404, description: 'Template not found'),
|
||||||
|
new OA\Response(response: 422, description: 'Template not yet approved'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
#[IsGranted('ROLE_ADMIN')]
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
#[Route('/api/v1/sms/send-template', methods: ['POST'])]
|
#[Route('/api/v1/sms/send-template', methods: ['POST'])]
|
||||||
public function sendViaTemplate(Request $request): JsonResponse
|
public function sendViaTemplate(Request $request): JsonResponse
|
||||||
|
|||||||
Reference in New Issue
Block a user