feat(payment): add GET /api/v1/my/payments for the logged-in user
Add PaymentRepository::findByUser/countByUser and a paginated my/payments endpoint that returns the authenticated user's own payments (derived from the token, never a userId in the URL). Public dashboard's transactions tab can now list payments instead of hitting a nonexistent route. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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` بهروز شود.
|
||||
@@ -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
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user