diff --git a/controllers/admin/AdminSaferPayOfficialSettingsController.php b/controllers/admin/AdminSaferPayOfficialSettingsController.php index ebad0939..4bd0e358 100755 --- a/controllers/admin/AdminSaferPayOfficialSettingsController.php +++ b/controllers/admin/AdminSaferPayOfficialSettingsController.php @@ -693,17 +693,36 @@ private function getCurrencies() */ private function getPaymentMethodsData() { + /** @var SaferPayPaymentRepository $paymentRepository */ + $paymentRepository = $this->module->getService(SaferPayPaymentRepository::class); + try { - /** @var SaferPayObtainPaymentMethods $obtainMethods */ - $obtainMethods = $this->module->getService(SaferPayObtainPaymentMethods::class); - $paymentMethods = $obtainMethods->obtainPaymentMethodsNamesAsArray(); + // Re-read the account and reconcile the stored list when the Payment Methods + // settings open, so methods added/removed on the Saferpay account are reflected + // (and persisted for the front office) without requiring a Save click. Enabled + // flags are preserved by the refresh; newly added methods default to disabled. + /** @var SaferPayRefreshPaymentsService $refreshPaymentsService */ + $refreshPaymentsService = $this->module->getService(SaferPayRefreshPaymentsService::class); + $refreshPaymentsService->refreshPayments(); + + // The refresh persists the account's methods, so read them back from storage + // instead of calling the API a second time. + $paymentMethods = array_column($paymentRepository->getAllPaymentMethodsNames(), 'name'); + + // refreshPayments() is a no-op when nothing is active yet (e.g. a fresh setup), + // so fall back to the live account list to still surface newly available methods. + if (empty($paymentMethods)) { + /** @var SaferPayObtainPaymentMethods $obtainMethods */ + $obtainMethods = $this->module->getService(SaferPayObtainPaymentMethods::class); + $paymentMethods = $obtainMethods->obtainPaymentMethodsNamesAsArray(); + } } catch (SaferPayApiException $exception) { - return ['error' => $this->module->l('Failed to load payment methods. Please verify your API credentials.', self::FILE_NAME)]; + // Account unreachable (bad credentials / offline): keep the last-known stored + // list rather than wiping the page. Credential validity is surfaced separately + // on the Credentials tab. Never clears stored configuration. + $paymentMethods = array_column($paymentRepository->getAllPaymentMethodsNames(), 'name'); } - /** @var SaferPayPaymentRepository $paymentRepository */ - $paymentRepository = $this->module->getService(SaferPayPaymentRepository::class); - /** @var SaferPayLogoRepository $logoRepository */ $logoRepository = $this->module->getService(SaferPayLogoRepository::class); diff --git a/src/Repository/SaferPayPaymentRepository.php b/src/Repository/SaferPayPaymentRepository.php index 9b16e20c..99c48671 100644 --- a/src/Repository/SaferPayPaymentRepository.php +++ b/src/Repository/SaferPayPaymentRepository.php @@ -110,6 +110,21 @@ public function getActivePaymentMethodsNames() return $result; } + public function getAllPaymentMethodsNames() + { + $query = new DbQuery(); + $query->select('name'); + $query->from('saferpay_payment'); + + $result = Db::getInstance()->executeS($query); + + if (!$result) { + return []; + } + + return $result; + } + public function truncateTable() { $query = 'TRUNCATE TABLE ' . _DB_PREFIX_ . 'saferpay_payment;'; diff --git a/tests/Unit/Service/SaferPayRefreshPaymentsServiceTest.php b/tests/Unit/Service/SaferPayRefreshPaymentsServiceTest.php new file mode 100644 index 00000000..289d5fab --- /dev/null +++ b/tests/Unit/Service/SaferPayRefreshPaymentsServiceTest.php @@ -0,0 +1,137 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +namespace Invertus\SaferPay\Tests\Unit\Service; + +use Invertus\SaferPay\Logger\LoggerInterface; +use Invertus\SaferPay\Repository\SaferPayFieldRepository; +use Invertus\SaferPay\Repository\SaferPayPaymentRepository; +use Invertus\SaferPay\Repository\SaferPayRestrictionRepository; +use Invertus\SaferPay\Service\SaferPayObtainPaymentMethods; +use Invertus\SaferPay\Service\SaferPayRefreshPaymentsService; +use PHPUnit\Framework\TestCase; + +class SaferPayRefreshPaymentsServiceTest extends TestCase +{ + public function testReconcilePreservesEnabledAddsNewDisabledDropsRemoved() + { + // Stored: VISA (enabled), AMEX (enabled). Account: VISA (kept), TWINT (added), AMEX removed. + $paymentRepository = $this->mockPaymentRepository([ + ['name' => 'VISA', 'active' => '1'], + ['name' => 'AMEX', 'active' => '1'], + ]); + $fieldRepository = $this->mockFieldRepository(['VISA' => true, 'AMEX' => false]); + $obtainPaymentMethods = $this->mockObtainPaymentMethods(['VISA', 'TWINT']); + + // Both tables are rebuilt. + $paymentRepository->expects($this->once())->method('truncateTable'); + $fieldRepository->expects($this->once())->method('truncateTable'); + + // VISA keeps active=1; TWINT added as active=0; AMEX (removed) is never re-inserted. + $paymentRepository->expects($this->exactly(2)) + ->method('insertPayment') + ->withConsecutive( + [['name' => 'VISA', 'active' => 1]], + [['name' => 'TWINT', 'active' => 0]] + ); + // Custom-form flag preserved for VISA (true -> 1), default 0 for the new TWINT. + $fieldRepository->expects($this->exactly(2)) + ->method('insertField') + ->withConsecutive( + [['name' => 'VISA', 'active' => 1]], + [['name' => 'TWINT', 'active' => 0]] + ); + + $this->makeService($paymentRepository, $obtainPaymentMethods, $fieldRepository)->refreshPayments(); + } + + public function testDoesNothingWhenNoActivePaymentMethodsStored() + { + $paymentRepository = $this->mockPaymentRepository([]); + $fieldRepository = $this->mockFieldRepository([]); + $obtainPaymentMethods = $this->mockObtainPaymentMethods(['VISA']); + + // Early return: no API reconciliation and no destructive rebuild. + $obtainPaymentMethods->expects($this->never())->method('obtainPaymentMethodsNamesAsArray'); + $paymentRepository->expects($this->never())->method('truncateTable'); + $paymentRepository->expects($this->never())->method('insertPayment'); + + $this->makeService($paymentRepository, $obtainPaymentMethods, $fieldRepository)->refreshPayments(); + } + + private function makeService($paymentRepository, $obtainPaymentMethods, $fieldRepository) + { + return new SaferPayRefreshPaymentsService( + $paymentRepository, + $obtainPaymentMethods, + $this->createMockWithMethods(SaferPayRestrictionRepository::class, []), + $fieldRepository, + $this->createMockWithMethods(LoggerInterface::class, []) + ); + } + + private function mockPaymentRepository(array $activePayments) + { + $mock = $this->createMockWithMethods( + SaferPayPaymentRepository::class, + ['getActivePaymentMethods', 'truncateTable', 'insertPayment'] + ); + $mock->method('getActivePaymentMethods')->willReturn($activePayments); + + return $mock; + } + + private function mockFieldRepository(array $activeByName) + { + $mock = $this->createMockWithMethods( + SaferPayFieldRepository::class, + ['isActiveByName', 'truncateTable', 'insertField'] + ); + $mock->method('isActiveByName')->willReturnCallback(function ($name) use ($activeByName) { + return isset($activeByName[$name]) ? $activeByName[$name] : false; + }); + + return $mock; + } + + private function mockObtainPaymentMethods(array $names) + { + $mock = $this->createMockWithMethods( + SaferPayObtainPaymentMethods::class, + ['obtainPaymentMethodsNamesAsArray'] + ); + $mock->method('obtainPaymentMethodsNamesAsArray')->willReturn($names); + + return $mock; + } + + private function createMockWithMethods($class, array $methods) + { + $builder = $this->getMockBuilder($class)->disableOriginalConstructor(); + if (!empty($methods)) { + $builder->setMethods($methods); + } + + return $builder->getMock(); + } +}