feat(payment): unify payment callback endpoint for all gateways and types
This commit is contained in:
@@ -56,7 +56,8 @@ class AppointmentPaidConfirmFilesSessionTest extends ApiTestCase
|
||||
|
||||
private function fireCallback(Payment $payment): void
|
||||
{
|
||||
$this->client->request('POST', '/api/v1/payment/callback/mock?' . http_build_query([
|
||||
// بدون `gateway` فرستاده میشود تا fallbackِ خواندن درگاه از رکورد پرداخت هم پوشش بخورد.
|
||||
$this->client->request('POST', '/api/v1/payment/callback?' . http_build_query([
|
||||
'order_id' => $payment->getOrderId(),
|
||||
'mock' => '1',
|
||||
'ResCode' => '0',
|
||||
|
||||
@@ -37,7 +37,8 @@ class PaymentCallbackAmountTest extends ApiTestCase
|
||||
|
||||
private function fireCallback(Payment $payment, int $reportedAmount): void
|
||||
{
|
||||
$this->client->request('POST', '/api/v1/payment/callback/mock?' . http_build_query([
|
||||
$this->client->request('POST', '/api/v1/payment/callback?' . http_build_query([
|
||||
'gateway' => 'mock',
|
||||
'order_id' => $payment->getOrderId(),
|
||||
'mock' => '1',
|
||||
'ResCode' => '0',
|
||||
@@ -91,7 +92,8 @@ class PaymentCallbackAmountTest extends ApiTestCase
|
||||
|
||||
private function fireCallbackWithRef(Payment $payment, string $refId, int $reportedAmount): void
|
||||
{
|
||||
$this->client->request('POST', '/api/v1/payment/callback/mock?' . http_build_query([
|
||||
$this->client->request('POST', '/api/v1/payment/callback?' . http_build_query([
|
||||
'gateway' => 'mock',
|
||||
'order_id' => $payment->getOrderId(),
|
||||
'mock' => '1',
|
||||
'ResCode' => '0',
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Payment;
|
||||
|
||||
use App\Config\Entity\SiteConfig;
|
||||
use App\Payment\Entity\Payment;
|
||||
use App\Payment\Service\PaymentManager;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* Every gateway and every payment type shares one callback path. The gateway
|
||||
* and the order travel as query params so the URL registered with the bank
|
||||
* never has to change.
|
||||
*/
|
||||
class UnifiedPaymentCallbackTest extends ApiTestCase
|
||||
{
|
||||
private function enableTestMode(): void
|
||||
{
|
||||
$cfg = $this->em->getRepository(SiteConfig::class)->findOneBy(['configKey' => 'payment_test_mode']);
|
||||
if ($cfg === null) {
|
||||
$cfg = new SiteConfig('payment_test_mode', '1');
|
||||
$this->em->persist($cfg);
|
||||
} else {
|
||||
$cfg->setValue('1');
|
||||
}
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
private function makePayment(string $type, string $gateway = 'mock'): Payment
|
||||
{
|
||||
$payment = $this->stampTenant(new Payment($this->createUser(), 50000, $gateway, $type));
|
||||
$this->em->persist($payment);
|
||||
$this->em->flush();
|
||||
|
||||
return $payment;
|
||||
}
|
||||
|
||||
private function reload(Payment $payment): Payment
|
||||
{
|
||||
$this->em->clear();
|
||||
|
||||
return $this->em->getRepository(Payment::class)->find($payment->getId());
|
||||
}
|
||||
|
||||
/** @param array<string, string> $extra */
|
||||
private function fireCallback(Payment $payment, array $extra = []): void
|
||||
{
|
||||
$this->client->request('POST', PaymentManager::CALLBACK_PATH . '?' . http_build_query(array_merge([
|
||||
'order_id' => $payment->getOrderId(),
|
||||
'mock' => '1',
|
||||
'ResCode' => '0',
|
||||
'mock_amount' => '50000',
|
||||
], $extra)));
|
||||
}
|
||||
|
||||
/** @return array<string, array{string}> */
|
||||
public static function paymentTypeProvider(): array
|
||||
{
|
||||
return [
|
||||
'appointment' => [Payment::TYPE_APPOINTMENT],
|
||||
'subscription' => [Payment::TYPE_SUBSCRIPTION],
|
||||
'sms wallet' => [Payment::TYPE_SMS_WALLET],
|
||||
];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('paymentTypeProvider')]
|
||||
public function testEveryPaymentTypeUsesTheSameCallbackPath(string $type): void
|
||||
{
|
||||
$this->enableTestMode();
|
||||
$payment = $this->makePayment($type);
|
||||
|
||||
$url = static::getContainer()->get(PaymentManager::class)->callbackUrl($payment);
|
||||
|
||||
$this->assertStringContainsString(PaymentManager::CALLBACK_PATH . '?', $url);
|
||||
$this->assertStringNotContainsString('/subscription-payment/', $url);
|
||||
$this->assertStringContainsString('gateway=mock', $url);
|
||||
$this->assertStringContainsString('order_id=' . $payment->getOrderId(), $url);
|
||||
}
|
||||
|
||||
public function testCallbackAcceptsGatewayAsQueryParam(): void
|
||||
{
|
||||
$this->enableTestMode();
|
||||
$payment = $this->makePayment(Payment::TYPE_SMS_WALLET);
|
||||
|
||||
$this->fireCallback($payment, ['gateway' => 'mock']);
|
||||
|
||||
$this->assertSame(Payment::STATUS_SUCCESS, $this->reload($payment)->getStatus());
|
||||
}
|
||||
|
||||
public function testCallbackFallsBackToTheGatewayStoredOnThePayment(): void
|
||||
{
|
||||
$this->enableTestMode();
|
||||
$payment = $this->makePayment(Payment::TYPE_SMS_WALLET);
|
||||
|
||||
$this->fireCallback($payment);
|
||||
|
||||
$this->assertSame(Payment::STATUS_SUCCESS, $this->reload($payment)->getStatus());
|
||||
}
|
||||
|
||||
public function testUnknownOrderWithoutGatewayIsNotFound(): void
|
||||
{
|
||||
$this->enableTestMode();
|
||||
|
||||
$this->client->request('POST', PaymentManager::CALLBACK_PATH . '?' . http_build_query([
|
||||
'order_id' => 'ORD-DOES-NOT-EXIST',
|
||||
'mock' => '1',
|
||||
'ResCode' => '0',
|
||||
]));
|
||||
|
||||
$this->assertSame(200, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertStringContainsString('یافت نشد', (string) $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testRemovedLegacyCallbackRoutesReturn404(): void
|
||||
{
|
||||
$this->enableTestMode();
|
||||
$payment = $this->makePayment(Payment::TYPE_SUBSCRIPTION);
|
||||
$query = '?' . http_build_query(['order_id' => $payment->getOrderId()]);
|
||||
|
||||
foreach (['/api/v1/payment/callback/mock', '/api/v1/subscription-payment/callback/mock'] as $legacy) {
|
||||
$this->client->request('POST', $legacy . $query);
|
||||
$this->assertSame(404, $this->client->getResponse()->getStatusCode(), $legacy);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ class ApiLeastPrivilegeTest extends ApiTestCase
|
||||
'practice_domain_list' => 'حوزههای فعالیت — دادهٔ مرجع',
|
||||
'app_subscription_subscription_plans' => 'پلنهای اشتراک — کاتالوگ عمومی',
|
||||
'app_payment_payment_config' => 'نام درگاهها و کارمزد — بدون مقدار محرمانه',
|
||||
'app_payment_payment_callback' => 'callback بانک — عمدا PUBLIC؛ بدون order_id معتبر فقط صفحهٔ «یافت نشد» میدهد',
|
||||
'app_representation_sitecontext_resolve' => 'حل دامنه به شهر/نماینده — ورودی رندر سایت',
|
||||
'resource_strategies' => 'فهرست ثابتِ استراتژیهای تخصیص منبع',
|
||||
'app_clinicservice_clinicservice_listservicecategories' => 'دستههای ثابت خدمت (سرپایی/بستری)',
|
||||
@@ -116,7 +117,6 @@ class ApiLeastPrivilegeTest extends ApiTestCase
|
||||
'app_auth_auth_resetpassword' => 'بازیابی رمز',
|
||||
'app_auth_preregistration_submit' => 'پیشثبتنام عمومی',
|
||||
'app_payment_payment_callback' => 'کالبک درگاه — بدون توکن فراخوانی میشود',
|
||||
'app_payment_payment_subscriptioncallback' => 'کالبک درگاه اشتراک',
|
||||
|
||||
// ── اکشن روی دادهٔ خودِ کاربر: منبعی در رجیستری ندارد ─────────────────
|
||||
'app_auth_auth_changepassword' => 'تغییر رمزِ خودِ کاربر',
|
||||
|
||||
Reference in New Issue
Block a user