Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions src/Service/SaferPayGetTerminals.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 '';
}
}
4 changes: 3 additions & 1 deletion src/Service/SettingsTranslationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
119 changes: 119 additions & 0 deletions tests/Unit/Service/SaferPayGetTerminalsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php
/**
*NOTICE OF LICENSE
*
*This source file is subject to the Open Software License (OSL 3.0)
*that is bundled with this package in the file LICENSE.txt.
*It is also available through the world-wide-web at this URL:
*http://opensource.org/licenses/osl-3.0.php
*If you did not receive a copy of the license and are unable to
*obtain it through the world-wide-web, please send an email
*to license@prestashop.com so we can send you a copy immediately.
*
*DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
*versions in the future. If you wish to customize PrestaShop for your
*needs please refer to http://www.prestashop.com for more information.
*
*@author INVERTUS UAB www.invertus.eu <support@invertus.eu>
*@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'])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The setMethods() method is deprecated in PHPUnit 8/9 and has been completely removed in PHPUnit 10. Since getTerminals is an existing method on GetTerminalsService, you should use onlyMethods() instead to ensure compatibility with newer PHPUnit versions.

            ->onlyMethods(['getTerminals'])

->getMock();
$mock->method('getTerminals')->willReturn($response);

return $mock;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(',')
Expand Down Expand Up @@ -202,15 +206,15 @@ export function ApiCredentials() {
<div className="sp-relative">
<Input
id="api-password"
type={showApiPassword && !isStoredPasswordMasked ? 'text' : 'password'}
type={showPasswordToggle && showApiPassword ? 'text' : 'password'}
placeholder={t('enterApiPassword', envLabel.toLowerCase())}
value={password}
onChange={(e) => setField('password', e.target.value)}
className={isStoredPasswordMasked ? undefined : 'sp-pr-10'}
required
aria-required="true"
/>
{!isStoredPasswordMasked && (
{showPasswordToggle && (
<button
type="button"
onClick={() => setShowApiPassword(!showApiPassword)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,13 @@ export function PaymentProcessing() {
</CardContent>
</Card>

{/* Card Display & Saving */}
{/* Card Display */}
<Card>
<CardHeader>
<div className="sp-flex sp-items-center sp-gap-2">
<CreditCard className="sp-h-5 sp-w-5 sp-text-muted-foreground" />
<div className="sp-flex sp-flex-col sp-gap-1.5">
<CardTitle className="sp-text-base sp-font-semibold">{t('cardDisplaySaving')}</CardTitle>
<CardTitle className="sp-text-base sp-font-semibold">{t('cardDisplay')}</CardTitle>
<CardDescription>
{t('cardDisplayDescription')}
</CardDescription>
Expand Down Expand Up @@ -263,14 +263,26 @@ export function PaymentProcessing() {
/>
</div>
)}
</div>
</CardContent>
</Card>

<div className="sp-flex sp-flex-col sp-gap-3 sp-pt-2">
<div className="sp-flex sp-flex-col sp-gap-1">
<Label className="sp-text-sm sp-font-medium">{t('creditCardSaving')}</Label>
<p className="sp-text-xs sp-text-muted-foreground">
{t('creditCardSavingDescription')}
</p>
</div>
{/* Card Saving for Customers */}
<Card>
<CardHeader>
<div className="sp-flex sp-items-center sp-gap-2">
<CreditCard className="sp-h-5 sp-w-5 sp-text-muted-foreground" />
<div className="sp-flex sp-flex-col sp-gap-1.5">
<CardTitle className="sp-text-base sp-font-semibold">{t('cardSavingForCustomers')}</CardTitle>
<CardDescription>
{t('creditCardSavingDescription')}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<div className="sp-grid sp-gap-4">
<div className="sp-flex sp-flex-col sp-gap-3">
<RadioGroup
value={String(settings.creditCardSave)}
onValueChange={(val) => updateSettings({ creditCardSave: Number(val) })}
Expand Down
Loading