diff --git a/.claude/prompt/my-payments-list-endpoint.md b/.claude/prompt/my-payments-list-endpoint.md new file mode 100644 index 00000000..c37ba8dc --- /dev/null +++ b/.claude/prompt/my-payments-list-endpoint.md @@ -0,0 +1,133 @@ +# افزودن endpoint لیست پرداخت‌های کاربر لاگین‌شده (`GET /api/v1/my/payments`) + +## پروژه + +`clinicpro` (Backend). **این پرامپت اول اجرا شود.** + +> **Cross-repo:** سایت عمومی (`nobat724_front`) این endpoint را در تب «تراکنش‌ها»ی داشبورد مصرف می‌کند. پرامپت همتای frontend: +> `nobat724_front/.claude/prompt/fix-dashboard-uuid-and-lists.md` + +## زمینه + +داشبورد کاربر در سایت عمومی تب «تراکنش‌ها» دارد که `request.getMyPayments(userId)` را صدا می‌زند → `GET /api/v1/payment/my-payments/{userId}`. اما **چنین route‌ای در backend وجود ندارد** (تأییدشده با `debug:router`): تنها endpointهای پرداخت، `GET /api/v1/payment/{uuid}` (تک پرداخت) و `GET /api/v1/admin/payments` (ادمین) هستند. در نتیجه تب تراکنش‌ها همیشه **404** می‌گیرد. + +`Payment` به `user` (پرداخت‌کننده) لینک دارد و `toArray()` دارد، ولی `PaymentRepository` متد `findByUser` ندارد. + +## مشکل / هدف + +یک endpoint عمومی-برای-کاربر بساز: `GET /api/v1/my/payments` که **پرداخت‌های خودِ کاربر لاگین‌شده** را (از روی توکن، نه از روی userId در URL) به‌صورت لیست برمی‌گرداند. الگو دقیقاً مثل `MyAppointmentsController` و قرارداد پاسخ مثل بقیه‌ی listهای کاربر. + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `src/Payment/Controller/PaymentController.php` | افزودن متد `myPayments()` (یا کنترلر `MyPaymentsController` جدید) | +| `src/Payment/Repository/PaymentRepository.php` | افزودن `findByUser(User $user, ?string $status, int $page, int $limit)` | +| `src/Payment/Entity/Payment.php` | `getUser()`, `toArray()` موجود | +| `src/Appointment/Controller/MyAppointmentsController.php` | مرجع الگو (CurrentUser, paginated) | +| `docs/api/payment.md` | مستندسازی endpoint جدید | + +## وضعیت فعلی (کد واقعی) + +### `PaymentRepository` — بدون findByUser + +```php +public function findByUuid(string $uuid): ?Payment { ... } +public function findByOrderId(string $orderId): ?Payment { ... } +public function save(Payment $entity, bool $flush = true): void { ... } +``` + +### `Payment` — لینک user + toArray + +```php +#[ORM\JoinColumn(name: 'user_id', ...)] private User $user; +public function getUser(): User { return $this->user; } +public function getOrderId(): string { ... } +public function getAmountRials(): int { ... } +public function getStatus(): string { ... } +public function toArray(): array { ... } // شکل پرداخت +``` + +### route ناموجود که فرانت صدا می‌زند + +``` +GET /api/v1/payment/my-payments/{userId} → 404 (وجود ندارد) +``` + +## وظایف + +### ۱. `findByUser` در `PaymentRepository` + +```php +/** @return Payment[] */ +public function findByUser(User $user, ?string $status = null, int $page = 1, int $limit = 20): array +{ + $qb = $this->createQueryBuilder('p') + ->where('p.user = :user')->setParameter('user', $user) + ->orderBy('p.createdAt', 'DESC') + ->setFirstResult(($page - 1) * $limit) + ->setMaxResults($limit); + if ($status !== null) { + $qb->andWhere('p.status = :status')->setParameter('status', $status); + } + return $qb->getQuery()->getResult(); +} + +public function countByUser(User $user, ?string $status = null): int +{ + $qb = $this->createQueryBuilder('p')->select('COUNT(p.id)') + ->where('p.user = :user')->setParameter('user', $user); + if ($status !== null) { + $qb->andWhere('p.status = :status')->setParameter('status', $status); + } + return (int) $qb->getQuery()->getSingleScalarResult(); +} +``` + +> از `User` به‌جای `user_id` در DQL استفاده کن (`App\Auth\Entity\User` را import کن). status اختیاری (`pending`/`paid`/`failed`/...). + +### ۲. endpoint `GET /api/v1/my/payments` + +در `PaymentController` (یا کنترلر جدید `MyPaymentsController` هم‌سبک با `MyAppointmentsController`): + +```php +#[Route('/api/v1/my/payments', methods: ['GET'])] +#[IsGranted('IS_AUTHENTICATED_FULLY')] +public function myPayments(Request $request, #[CurrentUser] User $user): JsonResponse +{ + $page = max(1, (int) $request->query->get('page', 1)); + $limit = min(100, max(1, (int) $request->query->get('limit', 20))); + $status = $request->query->get('status'); + + $items = array_map( + fn(Payment $p) => $p->toArray(), + $this->paymentRepo->findByUser($user, $status, $page, $limit) + ); + $total = $this->paymentRepo->countByUser($user, $status); + + return $this->paginated($items, $total, $page, $limit); +} +``` + +> از `paginated()` استفاده کن (نه success تودرتو) تا فرانت items را از `data` و total را از `meta.totalRecords` بخواند — هم‌خوان با بقیه‌ی listها. **کاربر فقط پرداخت‌های خودش را می‌بیند** (از `#[CurrentUser]`؛ هیچ userId از URL گرفته نمی‌شود). + +### ۳. مطمئن شو route عمومی/auth درست است + +- `/api/v1/my/payments` زیر فایروال `^/api` با `IS_AUTHENTICATED_FULLY` می‌افتد (نیاز به whitelist عمومی ندارد چون کاربر لاگین است). نیازی به تغییر `security.yaml` نیست. تأیید کن با `debug:router`. + +### ۴. مستندسازی + +در `docs/api/payment.md`: endpoint `GET /api/v1/my/payments` با permission، query params (`page`, `limit`, `status`)، و نمونه‌ی پاسخ paginated واقعی (items + meta). + +## نکات مهم + +- **هیچ migration لازم نیست** (فقط query + route). +- **امنیت:** هرگز `userId` از URL نگیر؛ فقط `#[CurrentUser]`. کاربر نباید پرداخت دیگران را ببیند. +- پاسخ‌ها از `BaseController`؛ این endpoint باید `paginated()` بدهد (نه `success(['data'=>...])`) — فرانت `data` (آرایه) و `meta.totalRecords` می‌خواند. +- `toArray()` پرداخت شکل واقعی را می‌دهد؛ اگر فیلدی برای نمایش لازم است (مبلغ، وضعیت، تاریخ، نوع، order_id، appointment_uuid) و در `toArray` نیست، اضافه کن. +- تست: + - `ddev exec php -l` روی فایل‌های تغییر یافته + - `ddev exec php bin/console cache:clear` + - `ddev exec php bin/console debug:router | grep "my/payments"` + - با توکن یک کاربر (مثلاً `09210651788` که یک پرداخت آزمایشی دارد): `GET /api/v1/my/payments` → ۲۰۰ با لیست پرداخت‌های همان کاربر؛ بدون توکن → ۴۰۱. + - `docs/api/payment.md` به‌روز شود. diff --git a/src/Payment/Controller/PaymentController.php b/src/Payment/Controller/PaymentController.php index 9331ac98..80b1a1e2 100644 --- a/src/Payment/Controller/PaymentController.php +++ b/src/Payment/Controller/PaymentController.php @@ -518,6 +518,23 @@ class PaymentController extends BaseController ]); } + #[IsGranted('IS_AUTHENTICATED_FULLY')] + #[Route('/api/v1/my/payments', methods: ['GET'])] + public function myPayments(Request $request, #[CurrentUser] User $user): JsonResponse + { + $page = max(1, (int) $request->query->get('page', 1)); + $limit = min(100, max(1, (int) $request->query->get('limit', 20))); + $status = $request->query->get('status'); + + $items = array_map( + fn(Payment $p) => $p->toArray(), + $this->paymentRepo->findByUser($user, $status, $page, $limit) + ); + $total = $this->paymentRepo->countByUser($user, $status); + + return $this->paginated($items, $total, $page, $limit); + } + #[IsGranted('IS_AUTHENTICATED_FULLY')] #[Route('/api/v1/payment/{uuid}', methods: ['GET'])] public function getStatus(string $uuid, #[CurrentUser] User $user): JsonResponse diff --git a/src/Payment/Repository/PaymentRepository.php b/src/Payment/Repository/PaymentRepository.php index b74ad2b1..a9d83477 100644 --- a/src/Payment/Repository/PaymentRepository.php +++ b/src/Payment/Repository/PaymentRepository.php @@ -2,6 +2,7 @@ namespace App\Payment\Repository; +use App\Auth\Entity\User; use App\Payment\Entity\Payment; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; @@ -18,6 +19,35 @@ class PaymentRepository extends ServiceEntityRepository return $this->findOneBy(['uuid' => $uuid]); } + /** @return Payment[] */ + public function findByUser(User $user, ?string $status = null, int $page = 1, int $limit = 20): array + { + $qb = $this->createQueryBuilder('p') + ->where('p.user = :user')->setParameter('user', $user) + ->orderBy('p.createdAt', 'DESC') + ->setFirstResult(($page - 1) * $limit) + ->setMaxResults($limit); + + if ($status !== null) { + $qb->andWhere('p.status = :status')->setParameter('status', $status); + } + + return $qb->getQuery()->getResult(); + } + + public function countByUser(User $user, ?string $status = null): int + { + $qb = $this->createQueryBuilder('p') + ->select('COUNT(p.id)') + ->where('p.user = :user')->setParameter('user', $user); + + if ($status !== null) { + $qb->andWhere('p.status = :status')->setParameter('status', $status); + } + + return (int) $qb->getQuery()->getSingleScalarResult(); + } + public function findByOrderId(string $orderId): ?Payment { return $this->findOneBy(['orderId' => $orderId]);