Skip to content
Open
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
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,5 @@
- BO : Fixed issue when the "Could not reach your Saferpay account" warning kept showing after payment methods had loaded successfully
- Fixed issue when files removed in this version stayed on disk after an upgrade, leaving obsolete iframe checkout controllers reachable and re-creating obsolete menu tabs on module reset
- BO : Fixed issue when a saved API password offered no visible way to enter a new one, and browser password manager icons covered the show/hide password control
- FO : Removed the Saferpay Management API call from the checkout, payment methods are now read from local storage
- BO : Added a log entry when Saferpay stops offering a payment method, so a method disappearing from the settings and the checkout can be traced
16 changes: 9 additions & 7 deletions saferpayofficial.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@
use Invertus\SaferPay\Install\Uninstaller;
use Invertus\SaferPay\Service\SaferPayCartService;
use Invertus\SaferPay\Provider\PaymentTypeProvider;
use Invertus\SaferPay\Service\SaferPayObtainPaymentMethods;
use Invertus\SaferPay\Service\SaferPayStoredPaymentMethods;
use Invertus\SaferPay\Repository\SaferPayPaymentRepository;
use Invertus\SaferPay\Exception\Api\SaferPayApiException;
use Invertus\SaferPay\Service\PaymentRestrictionValidation;
use Invertus\SaferPay\Provider\CurrencyProvider;
use Invertus\SaferPay\Service\SaferPayEmailTemplateControlServiceInterface;
Expand Down Expand Up @@ -217,14 +216,17 @@ public function hookPaymentOptions($params)
/** @var PaymentTypeProvider $paymentTypeProvider */
$paymentTypeProvider = $this->getService(PaymentTypeProvider::class);

/** @var SaferPayObtainPaymentMethods $obtainPaymentMethods */
$obtainPaymentMethods = $this->getService(SaferPayObtainPaymentMethods::class);
/** @var SaferPayStoredPaymentMethods $storedPaymentMethods */
$storedPaymentMethods = $this->getService(SaferPayStoredPaymentMethods::class);
/** @var SaferPayPaymentRepository $paymentRepository */
$paymentRepository = $this->getService(SaferPayPaymentRepository::class);

try {
$paymentMethods = $obtainPaymentMethods->obtainPaymentMethods();
} catch (SaferPayApiException $exception) {
// Read the account's payment methods from storage. PrestaShop re-renders the payment
// step over AJAX on every address and carrier change, so calling the Management API
// here meant several GetTerminal calls per order.
$paymentMethods = $storedPaymentMethods->getPaymentMethods();

if (empty($paymentMethods)) {
return [];
}

Expand Down
2 changes: 2 additions & 0 deletions src/Install/Installer.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ private function installSaferPayPaymentTable()
`id_saferpay_payment` INTEGER(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(64) NOT NULL,
`active` tinyint(1) DEFAULT 0,
`logo_url` VARCHAR(255) DEFAULT NULL,
`currencies` VARCHAR(1024) DEFAULT NULL,
UNIQUE (`name`)
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci'
);
Expand Down
15 changes: 15 additions & 0 deletions src/Repository/SaferPayPaymentRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,21 @@ public function getActivePaymentMethodsNames()
return $result;
}

public function getAllPaymentMethods()
{
$query = new DbQuery();
$query->select('*');
$query->from('saferpay_payment');

$result = Db::getInstance()->executeS($query);

if (!$result) {
return [];
}

return $result;
}

public function getAllPaymentMethodsNames()
{
$query = new DbQuery();
Expand Down
90 changes: 72 additions & 18 deletions src/Service/SaferPayRefreshPaymentsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,48 +61,102 @@ public function __construct(

public function refreshPayments()
{
// Get enabled payments.
$activePayments = $this->paymentRepository->getActivePaymentMethods();

if (empty($activePayments)) {
$this->logger->info('No active payment options found', [
'context' => [],
]);

return;
}

// Get payments from API.
try {
$paymentsFromAPI = $this->obtainPayments->obtainPaymentMethodsNamesAsArray();
$paymentsFromAPI = $this->obtainPayments->obtainPaymentMethods();
} catch (Exception $exception) {
throw new SaferPayApiException('Initialize API failed', SaferPayApiException::INITIALIZE);
}

// Read every stored row, not only the enabled ones, so that a method the merchant
// deliberately switched off keeps its flags across a refresh instead of silently
// reappearing as enabled-by-default.
$paymentsInfo = [];
foreach ($activePayments as $payment) {
$paymentsInfo[$payment['name']]['name'] = $payment['name'];
foreach ($this->paymentRepository->getAllPaymentMethods() as $payment) {
$paymentsInfo[$payment['name']]['active'] = $payment['active'];
$paymentsInfo[$payment['name']]['field'] = $this->fieldRepository->isActiveByName($payment['name']);
}

$paymentNamesFromAPI = [];
foreach ($paymentsFromAPI as $payment) {
$paymentNamesFromAPI[] = $this->getPaymentName($payment);
}

// Logged before the rebuild so that a failure part way through the inserts still
// leaves a record of what the account stopped offering.
$this->logRemovedPayments($paymentsInfo, $paymentNamesFromAPI);

// Truncate tables.
$this->paymentRepository->truncateTable();
$this->fieldRepository->truncateTable();

foreach ($paymentsFromAPI as $payment) {
$paymentActive = (isset($paymentsInfo[$payment]['active'])) ? (int) $paymentsInfo[$payment]['active'] : 0;
$fieldActive = (isset($paymentsInfo[$payment]['field'])) ? (int) $paymentsInfo[$payment]['field'] : 0;
$paymentName = $this->getPaymentName($payment);
$paymentActive = (isset($paymentsInfo[$paymentName]['active'])) ? (int) $paymentsInfo[$paymentName]['active'] : 0;
$fieldActive = (isset($paymentsInfo[$paymentName]['field'])) ? (int) $paymentsInfo[$paymentName]['field'] : 0;

// The logo and the supported currencies are the only two things the checkout
// needed the account for. Persisting them here is what lets hookPaymentOptions
// build the payment list without calling the Management API on every render.
$currencies = isset($payment['currencies']) && is_array($payment['currencies'])
? $payment['currencies']
: [];

$this->paymentRepository->insertPayment([
'name' => $payment,
'name' => pSQL($paymentName),
'active' => $paymentActive,
'logo_url' => pSQL((string) $payment['logoUrl']),
'currencies' => pSQL(implode(',', $currencies)),
]);

$this->fieldRepository->insertField([
'name' => $payment,
'name' => pSQL($paymentName),
'active' => $fieldActive,
]);
}
}

/**
* @param array $payment
*
* @return string
*/
private function getPaymentName(array $payment)
{
return str_replace(' ', '', $payment['paymentMethod']);
}

/**
* A method that disappears from the Saferpay account is dropped from storage without
* leaving any trace, so the merchant finds it gone from both the settings page and the
* checkout with nothing to explain why. Logging it is what lets support tell them that
* Saferpay stopped offering it, instead of the module having lost it.
*
* @param array $storedPayments
* @param array $paymentNamesFromAPI
*
* @return void
*/
private function logRemovedPayments(array $storedPayments, array $paymentNamesFromAPI)
{
$removedPayments = array_diff(array_keys($storedPayments), $paymentNamesFromAPI);

foreach ($removedPayments as $paymentName) {
$message = sprintf(
'Payment method "%s" is no longer available on the Saferpay account and was removed from this shop',
$paymentName
);

if (empty($storedPayments[$paymentName]['active'])) {
$this->logger->notice($message, ['context' => []]);

continue;
}

$this->logger->warning(
sprintf('%s. It was enabled, so it is no longer offered in the checkout', $message),
['context' => []]
);
}
}
}
117 changes: 117 additions & 0 deletions src/Service/SaferPayStoredPaymentMethods.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?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\Service;

use Exception;
use Invertus\SaferPay\Logger\LoggerInterface;
use Invertus\SaferPay\Repository\SaferPayPaymentRepository;

if (!defined('_PS_VERSION_')) {
exit;
}

/**
* Serves the checkout the payment method list that used to come from the Management API.
*
* The account is only asked when nothing is stored yet, so a shop that has never opened the
* settings page still recovers on its own instead of showing an empty payment step.
*/
class SaferPayStoredPaymentMethods
{
const FILE_NAME = 'SaferPayStoredPaymentMethods';

/** @var SaferPayPaymentRepository */
private $paymentRepository;

/** @var SaferPayRefreshPaymentsService */
private $refreshPaymentsService;

/** @var LoggerInterface */
private $logger;

public function __construct(
SaferPayPaymentRepository $paymentRepository,
SaferPayRefreshPaymentsService $refreshPaymentsService,
LoggerInterface $logger
) {
$this->paymentRepository = $paymentRepository;
$this->refreshPaymentsService = $refreshPaymentsService;
$this->logger = $logger;
}

public function getPaymentMethods(): array
{
$paymentMethods = $this->readStoredPaymentMethods();

if ($this->isPopulated($paymentMethods)) {
return $paymentMethods;
}

try {
$this->refreshPaymentsService->refreshPayments();
} catch (Exception $exception) {
$this->logger->error(sprintf('%s - failed to populate the stored payment methods', self::FILE_NAME), [
'context' => [],
'exception' => $exception,
]);

return [];
}

return $this->readStoredPaymentMethods();
}

/**
* Rows written before the logo and currencies were stored carry neither, and so cannot
* drive the checkout. Every method the account returns has a logo, so its absence across
* the board means the upgrade backfill never ran or could not reach the account.
*/
private function isPopulated(array $paymentMethods): bool
{
foreach ($paymentMethods as $paymentMethod) {
if ($paymentMethod['logoUrl'] !== '') {
return true;
}
}

return false;
}

private function readStoredPaymentMethods(): array
{
$paymentMethods = [];

foreach ($this->paymentRepository->getAllPaymentMethods() as $payment) {
$currencies = empty($payment['currencies']) ? [] : explode(',', $payment['currencies']);

$paymentMethods[$payment['name']] = [
'paymentMethod' => $payment['name'],
'logoUrl' => (string) $payment['logo_url'],
'currencies' => $currencies,
];
}

return $paymentMethods;
}
}
42 changes: 41 additions & 1 deletion upgrade/install-2.1.0.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,19 @@
*@license SIX Payment Services
*/

use Invertus\SaferPay\Service\SaferPayRefreshPaymentsService;

if (!defined('_PS_VERSION_')) {
exit;
}

function upgrade_module_2_1_0()
function upgrade_module_2_1_0($module)
{
saferpayofficial_2_1_0_delete_removed_tabs();
saferpayofficial_2_1_0_delete_removed_files();
saferpayofficial_2_1_0_delete_removed_configuration();
saferpayofficial_2_1_0_add_payment_method_details();
saferpayofficial_2_1_0_backfill_payment_method_details($module);

Tools::clearSmartyCache();

Expand Down Expand Up @@ -154,3 +158,39 @@ function saferpayofficial_2_1_0_delete_removed_configuration()
{
Configuration::deleteByName('SAFERPAY_HOSTED_FIELDS_TEMPLATE');
}

/**
* The checkout used to read the logo and the supported currencies straight off the Management
* API on every payment step render. Storing them alongside the method removes that call.
*/
function saferpayofficial_2_1_0_add_payment_method_details()
{
$table = _DB_PREFIX_ . 'saferpay_payment';
$columns = array_column(Db::getInstance()->executeS('SHOW COLUMNS FROM `' . $table . '`'), 'Field');

if (!in_array('logo_url', $columns, true)) {
Db::getInstance()->execute('ALTER TABLE `' . $table . '` ADD `logo_url` VARCHAR(255) DEFAULT NULL');
}

if (!in_array('currencies', $columns, true)) {
Db::getInstance()->execute('ALTER TABLE `' . $table . '` ADD `currencies` VARCHAR(1024) DEFAULT NULL');
}
}

/**
* Populate the two new columns for shops that already have payment methods stored. Best effort
* on purpose: an unreachable account must not fail the upgrade, and the checkout repopulates
* on its own when it finds the columns still empty.
*/
function saferpayofficial_2_1_0_backfill_payment_method_details($module)
{
try {
/** @var SaferPayRefreshPaymentsService $refreshPaymentsService */
$refreshPaymentsService = $module->getService(SaferPayRefreshPaymentsService::class);
$refreshPaymentsService->refreshPayments();
} catch (Exception $exception) {
PrestaShopLogger::addLog(
'SaferPay 2.1.0 upgrade: could not backfill payment method details - ' . $exception->getMessage()
);
}
}
Loading