Files
clinicpro/src/Payment/Repository/PaymentRepository.php
T
hamed c247ac2c80 feat(payment): implement PaymentManager for handling payment logic and callbacks
- Refactor PaymentController to delegate payment processing to PaymentManager.
- Add findByOrderIdForUpdate method in PaymentRepository for pessimistic locking.
- Create PaymentLog entity and repository for auditing payment actions.
- Implement startGatewayHandoff and processCallback methods in PaymentManager.
- Introduce transaction handling and logging for payment verification.
- Update payment flow to ensure idempotency and prevent race conditions.
- Enhance security by logging sensitive actions without exposing credentials.
- Update database schema with migration for payment_logs table.
- Document changes in payment flow architecture.
2026-07-02 15:36:08 +03:30

119 lines
3.8 KiB
PHP

<?php
namespace App\Payment\Repository;
use App\Appointment\Entity\Appointment;
use App\Auth\Entity\User;
use App\Payment\Entity\Payment;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class PaymentRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Payment::class);
}
public function findByUuid(string $uuid): ?Payment
{
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]);
}
public function findByReferenceId(string $referenceId): ?Payment
{
return $this->findOneBy(['referenceId' => $referenceId]);
}
/**
* قفل بدبینانه روی ردیف پرداخت (باید داخل یک transaction فعال صدا زده شود).
* برای جلوگیری از verify هم‌زمانِ دو callback (race / double-verify).
*/
public function findByOrderIdForUpdate(string $orderId): ?Payment
{
return $this->createQueryBuilder('p')
->where('p.orderId = :o')->setParameter('o', $orderId)
->getQuery()
->setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)
->getOneOrNullResult();
}
public function findPendingByAppointment(Appointment $appointment): ?Payment
{
return $this->findOneBy([
'appointment' => $appointment,
'status' => Payment::STATUS_PENDING,
]);
}
/**
* Batch variant of findPendingByAppointment: all pending payments for the
* given appointments in ONE query, keyed by appointment id. Avoids the N+1
* in AppointmentExpiryService when many bookings expire at once.
*
* @param Appointment[] $appointments
* @return array<int, Payment> appointment id => pending Payment
*/
public function findPendingByAppointments(array $appointments): array
{
if ($appointments === []) {
return [];
}
$payments = $this->createQueryBuilder('p')
->andWhere('p.appointment IN (:appointments)')
->andWhere('p.status = :status')
->setParameter('appointments', $appointments)
->setParameter('status', Payment::STATUS_PENDING)
->getQuery()
->getResult();
$byAppointment = [];
foreach ($payments as $payment) {
$byAppointment[$payment->getAppointment()->getId()] = $payment;
}
return $byAppointment;
}
public function save(Payment $entity, bool $flush = true): void
{
$this->getEntityManager()->persist($entity);
if ($flush) $this->getEntityManager()->flush();
}
}