- 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.
119 lines
3.8 KiB
PHP
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();
|
|
}
|
|
}
|