feat(payment): add refund and reversal functionality to payment gateways

- Implemented `refund` and `reverse` methods in `PaymentGatewayInterface`.
- Added `PaymentRefundResult` class to handle refund operation results.
- Enhanced `MockGateway` and `SepGateway` to support refund and reversal operations.
- Updated `PaymentManager` to include `refundPayment` and `reversePayment` methods for handling refunds and reversals in transactions.
- Modified `ClinicSubscriptionRepository` and `SubscriptionService` to manage subscriptions during refunds.
- Added admin API endpoints for processing refunds and reversals.
- Updated security headers to allow form actions to the sandbox environment.
- Documented the new refund and reversal features in the API documentation.
This commit is contained in:
hamed
2026-07-02 18:45:34 +03:30
parent 6bb47d343d
commit 1b171a82f4
20 changed files with 1111 additions and 21 deletions
+38 -1
View File
@@ -37,6 +37,8 @@ class AdminApiController extends BaseController
private readonly EntityManagerInterface $em,
private readonly \App\Appointment\Service\SlotCalculatorService $slotCalculator,
private readonly \App\Insurance\Service\TenantInsuranceCleanupService $insuranceCleanup,
private readonly \App\Payment\Service\PaymentManager $paymentManager,
private readonly \App\Payment\Repository\PaymentRepository $paymentRepo,
) {}
// ── Users ─────────────────────────────────────────────────────────────────
@@ -891,7 +893,7 @@ class AdminApiController extends BaseController
{
$rows = $this->em->createQueryBuilder()
->select(
'p.uuid, p.orderId, p.amountRials, p.status, p.gateway, p.type, p.referenceId, p.createdAt, p.updatedAt',
'p.uuid, p.orderId, p.amountRials, p.status, p.gateway, p.type, p.referenceId, p.metadata, p.createdAt, p.updatedAt',
'u.mobileNumber as patient_mobile, u.realName as patient_name',
'a.uuid as appointment_uuid',
)
@@ -914,6 +916,8 @@ class AdminApiController extends BaseController
'gateway' => $p['gateway'],
'type' => $p['type'],
'ref_id' => $p['referenceId'],
'card_pan' => $p['metadata']['card_pan'] ?? null,
'refunds' => $p['metadata']['refunds'] ?? [],
'patient_mobile' => $p['patient_mobile'],
'patient_name' => $p['patient_name'],
'appointment_uuid' => $p['appointment_uuid'],
@@ -922,6 +926,39 @@ class AdminApiController extends BaseController
]);
}
#[Route('/api/v1/admin/payments/{uuid}/refund', methods: ['POST'])]
public function refundPayment(string $uuid, Request $request): JsonResponse
{
$payment = $this->paymentRepo->findOneBy(['uuid' => $uuid]);
if ($payment === null) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پرداخت یافت نشد', 404);
}
$amount = $request->toArray()['amount'] ?? null; // ریال؛ null = کل مبلغ
$result = $this->paymentManager->refundPayment(
$payment,
$amount !== null ? (int) $amount : null,
$request->getClientIp() ?? '',
);
return $result->success
? $this->success(['status' => $payment->getStatus(), 'refund_ref' => $result->refundRefId])
: $this->error(ErrorCodes::ERR_PAYMENT_002, $result->errorMessage, 422);
}
#[Route('/api/v1/admin/payments/{uuid}/reverse', methods: ['POST'])]
public function reversePayment(string $uuid, Request $request): JsonResponse
{
$payment = $this->paymentRepo->findOneBy(['uuid' => $uuid]);
if ($payment === null) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پرداخت یافت نشد', 404);
}
$result = $this->paymentManager->reversePayment($payment, $request->getClientIp() ?? '');
return $result->success
? $this->success(['status' => $payment->getStatus()])
: $this->error(ErrorCodes::ERR_PAYMENT_002, $result->errorMessage, 422);
}
// ── Representations ───────────────────────────────────────────────────────
#[OA\Get(