diff --git a/src/Service/SaferPayGetTerminals.php b/src/Service/SaferPayGetTerminals.php index 3c41d05d..cb3124bd 100644 --- a/src/Service/SaferPayGetTerminals.php +++ b/src/Service/SaferPayGetTerminals.php @@ -33,6 +33,11 @@ class SaferPayGetTerminals { + /** + * Terminal types that must never be offered for selection (not usable by this module). + */ + const EXCLUDED_TERMINAL_TYPES = ['MPO', 'SPG']; + /** @var GetTerminalsService */ private $getTerminalsService; @@ -64,17 +69,43 @@ public function fetchTerminalsWithCredentials($username, $password, $customerId, $terminalList = isset($response->Terminals) ? $response->Terminals : []; if (is_array($terminalList)) { foreach ($terminalList as $terminal) { - if (isset($terminal->TerminalId)) { - $terminals[] = [ - 'id' => $terminal->TerminalId, - 'name' => isset($terminal->Description) - ? $terminal->Description . ' (' . $terminal->TerminalId . ')' - : $terminal->TerminalId, - ]; + if (!isset($terminal->TerminalId)) { + continue; + } + + if (in_array($this->getTerminalType($terminal), self::EXCLUDED_TERMINAL_TYPES, true)) { + continue; } + + $terminals[] = [ + 'id' => $terminal->TerminalId, + 'name' => isset($terminal->Description) + ? $terminal->Description . ' (' . $terminal->TerminalId . ')' + : $terminal->TerminalId, + ]; } } return $terminals; } + + /** + * Reads the terminal type defensively: the Management API has been seen to expose it + * either as "Type" or "TerminalType". Returns an uppercased value (empty when absent). + * + * @param \stdClass $terminal + * @return string + */ + private function getTerminalType($terminal) + { + if (isset($terminal->Type) && is_scalar($terminal->Type)) { + return strtoupper((string) $terminal->Type); + } + + if (isset($terminal->TerminalType) && is_scalar($terminal->TerminalType)) { + return strtoupper((string) $terminal->TerminalType); + } + + return ''; + } } diff --git a/src/Service/SettingsTranslationService.php b/src/Service/SettingsTranslationService.php index 29772358..1ba0160c 100644 --- a/src/Service/SettingsTranslationService.php +++ b/src/Service/SettingsTranslationService.php @@ -194,7 +194,9 @@ private function getPaymentProcessingTranslations() 'beforeAuthorization' => $this->module->l('Before authorization', self::FILE_NAME), 'createBeforePayment' => $this->module->l('Create before payment', self::FILE_NAME), 'cardDisplaySaving' => html_entity_decode($this->module->l('Card Display & Saving', self::FILE_NAME), ENT_QUOTES, 'UTF-8'), - 'cardDisplayDescription' => $this->module->l('Configure how cards appear at checkout and whether customers can save them.', self::FILE_NAME), + 'cardDisplay' => $this->module->l('Card Display', self::FILE_NAME), + 'cardDisplayDescription' => $this->module->l('Configure how cards appear and are grouped at checkout.', self::FILE_NAME), + 'cardSavingForCustomers' => $this->module->l('Card Saving for Customers', self::FILE_NAME), 'groupCardsLabel' => $this->module->l('Group debit/credit cards as \'Cards\' in checkout', self::FILE_NAME), 'groupCardsDescription' => $this->module->l('If enabled, all supported card brands will be grouped and shown as a single \'Cards\' payment method at checkout.', self::FILE_NAME), 'showCardsLogo' => $this->module->l('Show \'Cards\' payment method logo', self::FILE_NAME), diff --git a/tests/Unit/Service/SaferPayGetTerminalsTest.php b/tests/Unit/Service/SaferPayGetTerminalsTest.php new file mode 100644 index 00000000..878333ed --- /dev/null +++ b/tests/Unit/Service/SaferPayGetTerminalsTest.php @@ -0,0 +1,119 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +namespace Invertus\SaferPay\Tests\Unit\Service; + +use Invertus\SaferPay\Api\Request\GetTerminalsService; +use Invertus\SaferPay\Service\SaferPayGetTerminals; +use PHPUnit\Framework\TestCase; + +class SaferPayGetTerminalsTest extends TestCase +{ + /** + * @dataProvider terminalTypePropertyProvider + */ + public function testExcludesMpoAndSpgKeepsOthersAndUntyped($typeProperty) + { + $service = new SaferPayGetTerminals($this->mockGetTerminalsService([ + $this->terminal('MPO_TERMINAL_ID', $typeProperty, 'MPO'), + $this->terminal('SPG_TERMINAL_ID', $typeProperty, 'SPG'), + $this->terminal('OTHER_TERMINAL_ID', $typeProperty, 'EMONEY'), + $this->terminal('NO_TYPE_TERMINAL_ID', $typeProperty, null), + ])); + + $ids = array_column( + $service->fetchTerminalsWithCredentials('u', 'p', 'cust', true), + 'id' + ); + + $this->assertNotContains('MPO_TERMINAL_ID', $ids); + $this->assertNotContains('SPG_TERMINAL_ID', $ids); + $this->assertContains('OTHER_TERMINAL_ID', $ids); + $this->assertContains('NO_TYPE_TERMINAL_ID', $ids); + } + + public function testExclusionIsCaseInsensitive() + { + $service = new SaferPayGetTerminals($this->mockGetTerminalsService([ + $this->terminal('LOWER_MPO', 'Type', 'mpo'), + $this->terminal('MIXED_SPG', 'Type', 'Spg'), + $this->terminal('KEEP', 'Type', 'card'), + ])); + + $ids = array_column( + $service->fetchTerminalsWithCredentials('u', 'p', 'cust', false), + 'id' + ); + + $this->assertSame(['KEEP'], $ids); + } + + public function testPreservesIdAndNameShape() + { + $service = new SaferPayGetTerminals($this->mockGetTerminalsService([ + $this->terminal('T1', 'Type', 'card', 'Main terminal'), + ])); + + $result = $service->fetchTerminalsWithCredentials('u', 'p', 'cust', true); + + $this->assertSame([['id' => 'T1', 'name' => 'Main terminal (T1)']], $result); + } + + public function terminalTypePropertyProvider() + { + // The Management API terminal-type property name is confirmed defensively: + // both "Type" and "TerminalType" are honoured. + return [ + 'Type property' => ['Type'], + 'TerminalType property' => ['TerminalType'], + ]; + } + + private function terminal($id, $typeProperty, $typeValue, $description = null) + { + $terminal = new \stdClass(); + $terminal->TerminalId = $id; + if ($description !== null) { + $terminal->Description = $description; + } + if ($typeValue !== null) { + $terminal->{$typeProperty} = $typeValue; + } + + return $terminal; + } + + private function mockGetTerminalsService(array $terminals) + { + $response = new \stdClass(); + $response->Terminals = $terminals; + + $mock = $this->getMockBuilder(GetTerminalsService::class) + ->disableOriginalConstructor() + ->setMethods(['getTerminals']) + ->getMock(); + $mock->method('getTerminals')->willReturn($response); + + return $mock; + } +} diff --git a/views/js/admin/settings-app/src/components/settings/api-credentials.tsx b/views/js/admin/settings-app/src/components/settings/api-credentials.tsx index 70896142..5c4cbe81 100644 --- a/views/js/admin/settings-app/src/components/settings/api-credentials.tsx +++ b/views/js/admin/settings-app/src/components/settings/api-credentials.tsx @@ -46,6 +46,10 @@ export function ApiCredentials() { const hasCredentials = username.length > 0 && password.length > 0 const hasBusinessLicense = isTest ? settings.testHasBusinessLicense : settings.liveHasBusinessLicense + // The show/hide toggle only appears once the merchant has typed a real password: the field + // must be non-empty and hold something other than the stored mask. + const showPasswordToggle = password.length > 0 && !isStoredPasswordMasked + const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ const invalidEmails = merchantEmails .split(',') @@ -202,7 +206,7 @@ export function ApiCredentials() {