Files
OpenCart4/admin/controller/payment/payanyway.php
T

565 lines
20 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace Opencart\Admin\Controller\Extension\PayAnyWay\Payment;
/**
* @package Opencart\Admin\Controller\Extension\PayAnyWay\Payment
*/
class PayAnyWay extends \Opencart\System\Engine\Controller
{
public const CMS_NAME = 'OpenCart';
public const MODULE_VERSION = '1.0.3';
public const MODULE_DEFAULT_SORT_ORDER = 0;
private const ROUTE_STARTUP_CODE = 'payanyway_post_route';
private const ROUTE_STARTUP_ACTION = 'catalog/extension/payanyway/startup/payanyway';
private const SUPPORTED_CURRENCIES = ['RUB'];
private const AVAILABLE_VAT_RATES = [
'vat_none' => '1105',
'vat0' => '1104',
'vat5' => '1108',
'vat7' => '1109',
'vat10' => '1103',
'vat22' => '1113',
'vat105' => '1110',
'vat107' => '1111',
'vat110' => '1107',
'vat122' => '1114',
];
private const PAYMENT_METHODS = [
'full_prepayment',
'prepayment',
'advance',
'full_payment',
'partial_payment',
'credit',
'credit_payment',
];
private const PAYMENT_OBJECTS = [
'commodity',
'excise',
'job',
'service',
'gambling_bet',
'gambling_prize',
'lottery',
'lottery_prize',
'intellectual_activity',
'payment',
'agent_commission',
'composite',
'another',
'property_right',
'sales_tax',
'resort_fee',
'award',
'non_operating',
'insurance_premium',
];
/** Canceled, Canceled Reversal, Voided штатной установки OpenCart. */
private const DEFAULT_CANCELED_ORDER_STATUS_IDS = [7, 9, 16];
private const RECEIPT_EMAIL_MAX_LENGTH = 64;
private const DEFAULT_PAYMENT_METHOD = 'full_payment';
private const DEFAULT_PAYMENT_OBJECT = 'commodity';
private array $errors = [];
/**
* @throws \Exception
*/
public function index(): void
{
$this->load->language('extension/payanyway/payment/payanyway');
$this->document->setTitle($this->language->get('heading_title'));
$data = [];
$this->applyValidationErrors($data);
$data['breadcrumbs'] = $this->getBreadcrumbs();
$warnings = $this->getWarnings($this->isFiscalizationEnabled(), (string)$this->config->get('payment_payanyway_receipt_email'));
$data['warning_startup'] = $warnings['startup'];
$data['warning_receipt_email'] = $warnings['receipt_email'];
$data['save'] = $this->url->link('extension/payanyway/payment/payanyway.save', 'user_token=' . $this->session->data['user_token']);
$data['back'] = $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment');
$data['payment_payanyway_status'] = $this->retrieveValue('payment_payanyway_status');
$data['payment_payanyway_account'] = $this->retrieveValue('payment_payanyway_account');
$data['integrity_code_placeholder'] = $this->hasIntegrityCode()
? $this->language->get('text_integrity_code_saved')
: $this->language->get('entry_integrity_code_description');
$data['payment_payanyway_server'] = $this->retrieveValue('payment_payanyway_server');
$data['payment_payanyway_test_mode'] = (string)$this->retrieveValue('payment_payanyway_test_mode');
$data['payment_payanyway_fiscalization'] = $this->isFiscalizationEnabled() ? '1' : '0';
$data['payment_payanyway_receipt_email'] = htmlspecialchars((string)$this->config->get('payment_payanyway_receipt_email'), ENT_QUOTES, 'UTF-8');
$data['payment_payanyway_payment_methods'] = $this->getOptions(self::PAYMENT_METHODS, 'entry_payment_method_');
$data['payment_payanyway_payment_method'] = $this->retrieveValueOrDefault('payment_payanyway_payment_method', self::DEFAULT_PAYMENT_METHOD);
$data['payment_payanyway_payment_objects'] = $this->getOptions(self::PAYMENT_OBJECTS, 'entry_payment_object_');
$data['payment_payanyway_payment_object'] = $this->retrieveValueOrDefault('payment_payanyway_payment_object', self::DEFAULT_PAYMENT_OBJECT);
$data['payment_payanyway_vat_rates'] = $this->getModuleVatRates();
$data['payment_payanyway_vat_rate'] = $this->retrieveValue('payment_payanyway_vat_rate');
$data['payment_payanyway_currencies'] = $this->getModuleCurrencies();
$data['payment_payanyway_currency'] = $this->retrieveValue('payment_payanyway_currency');
$data['order_statuses'] = $this->getAvailableOrderStatuses();
$data['payment_payanyway_pending_order_status_id'] = $this->retrieveValue('payment_payanyway_pending_order_status_id');
$data['payment_payanyway_success_order_status_id'] = $this->retrieveValue('payment_payanyway_success_order_status_id');
$data['payment_payanyway_error_order_status_id'] = $this->retrieveValue('payment_payanyway_error_order_status_id');
$data['payment_payanyway_canceled_order_status_ids'] = $this->getCanceledOrderStatusIds();
$data['geo_zones'] = $this->getAvailableGeoZones();
$data['payment_payanyway_geo_zone_id'] = $this->retrieveValue('payment_payanyway_geo_zone_id');
$data['payment_payanyway_sort_order'] = $this->retrieveValue('payment_payanyway_sort_order');
$data['callback_url'] = HTTP_CATALOG . 'index.php?route=extension/payanyway/payment/callback';
$data['header'] = $this->load->controller('common/header');
$data['column_left'] = $this->load->controller('common/column_left');
$data['footer'] = $this->load->controller('common/footer');
$this->response->setOutput($this->load->view('extension/payanyway/payment/payanyway', $data));
}
public function save(): void
{
$this->load->language('extension/payanyway/payment/payanyway');
$json = [];
if (!$this->user->hasPermission('modify', 'extension/payanyway/payment/payanyway')) {
$json['error'] = $this->language->get('error_permission');
$this->response->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode($json));
return;
}
if (!$this->validate()) {
$errorString = $this->language->get('error_validation') .
"<p style='white-space: pre-wrap; margin: 0;'>\n";
foreach ($this->errors as $error) {
$errorString .= $error . "\n";
}
$errorString = substr($errorString, 0, -1) . "</p>";
$json['error'] = $errorString;
}
if (!$json) {
$this->load->model('setting/setting');
$this->model_setting_setting->editSetting('payment_payanyway', $this->request->post);
if (!$this->isRouteStartupRegistered()) {
$this->registerRouteStartup();
}
$json['success'] = $this->language->get('text_success');
$json['payanyway_warnings'] = $this->getWarnings(
(bool)$this->request->post['payment_payanyway_fiscalization'],
(string)$this->request->post['payment_payanyway_receipt_email']
);
}
$this->response->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode($json));
}
/**
* @throws \Exception
*/
public function install(): void
{
if (!$this->user->hasPermission('modify', 'extension/payment')) {
return;
}
$defaultSettings = [
'payment_payanyway_status' => 0,
'payment_payanyway_account' => '',
'payment_payanyway_integrity_code' => '',
'payment_payanyway_server' => 'prod',
'payment_payanyway_test_mode' => 0,
'payment_payanyway_fiscalization' => 0,
'payment_payanyway_vat_rate' => self::AVAILABLE_VAT_RATES['vat_none'],
'payment_payanyway_receipt_email' => '',
'payment_payanyway_payment_method' => self::DEFAULT_PAYMENT_METHOD,
'payment_payanyway_payment_object' => self::DEFAULT_PAYMENT_OBJECT,
'payment_payanyway_canceled_order_status_ids' => self::DEFAULT_CANCELED_ORDER_STATUS_IDS,
'payment_payanyway_sort_order' => 0,
'payment_payanyway_cms_name' => $this->getCmsName(),
];
$orderStatuses = $this->getAvailableOrderStatuses();
$pendingStatus = array_filter($orderStatuses, function ($status) {
return (int)$status['order_status_id'] === 1;
});
if (!empty($pendingStatus)) {
$pendingKey = key($pendingStatus);
$defaultSettings['payment_payanyway_pending_order_status_id'] = $pendingStatus[$pendingKey]['order_status_id'];
}
$successStatus = array_filter($orderStatuses, function ($status) {
return (int)$status['order_status_id'] === 5;
});
if (!empty($successStatus)) {
$successKey = key($successStatus);
$defaultSettings['payment_payanyway_success_order_status_id'] = $successStatus[$successKey]['order_status_id'];
}
$errorStatus = array_filter($orderStatuses, function ($status) {
return (int)$status['order_status_id'] === 10;
});
if (!empty($errorStatus)) {
$errorKey = key($errorStatus);
$defaultSettings['payment_payanyway_error_order_status_id'] = $errorStatus[$errorKey]['order_status_id'];
}
$this->load->model('setting/setting');
$this->model_setting_setting->editSetting('payment_payanyway', $defaultSettings);
$this->registerRouteStartup();
}
public function uninstall(): void
{
if (!$this->user->hasPermission('modify', 'extension/payment')) {
return;
}
$this->load->model('setting/setting');
$this->model_setting_setting->deleteSetting('payment_payanyway');
$this->load->model('setting/startup');
$this->model_setting_startup->deleteStartupByCode(self::ROUTE_STARTUP_CODE);
}
/**
* Регистрирует startup витрины. Сигнатура addStartup() менялась: 4.0.0–4.0.1 —
* позиционные аргументы, 4.0.2 — массив, 4.1 — массив с обязательным description
* (лишний ключ 4.0.2 игнорирует).
*/
private function registerRouteStartup(): void
{
$this->load->model('setting/startup');
$this->model_setting_startup->deleteStartupByCode(self::ROUTE_STARTUP_CODE);
if (version_compare(VERSION, '4.0.2.0', '<')) {
$this->model_setting_startup->addStartup(self::ROUTE_STARTUP_CODE, self::ROUTE_STARTUP_ACTION, true, 0);
return;
}
$this->model_setting_startup->addStartup([
'code' => self::ROUTE_STARTUP_CODE,
'description' => 'PayAnyWay: Check/Pay URL методом POST',
'action' => self::ROUTE_STARTUP_ACTION,
'status' => 1,
'sort_order' => 0,
]);
}
private function isRouteStartupRegistered(): bool
{
$this->load->model('setting/startup');
return [] !== $this->model_setting_startup->getStartupByCode(self::ROUTE_STARTUP_CODE);
}
private function isRouteStartupActive(): bool
{
$this->load->model('setting/startup');
$startup = $this->model_setting_startup->getStartupByCode(self::ROUTE_STARTUP_CODE);
return !empty($startup['status']) && (($startup['action'] ?? '') === self::ROUTE_STARTUP_ACTION);
}
/**
* Предупреждения над формой настроек; пустая строка — предупреждения нет.
*
* @return array{startup: string, receipt_email: string}
*/
private function getWarnings(bool $fiscalization, string $receiptEmail): array
{
return [
'startup' => $this->isRouteStartupActive()
? ''
: sprintf(
$this->language->get('warning_startup_route'),
$this->url->link('marketplace/startup', 'user_token=' . $this->session->data['user_token'])
),
'receipt_email' => ($fiscalization && ('' === $receiptEmail))
? $this->language->get('warning_receipt_email')
: '',
];
}
private function validate(): bool
{
$this->errors = [];
if (!isset($this->request->post['payment_payanyway_account']) ||
!preg_match('/^[0-9]{1,20}$/D', (string)$this->request->post['payment_payanyway_account']) ||
((int)$this->request->post['payment_payanyway_account'] <= 0)
) {
$this->errors['account'] = $this->language->get('error_account');
}
// OpenCart экранирует весь POST (htmlspecialchars в system/library/request.php): код с & " < >
// сохранился бы экранированным, и подписи не сошлись бы с MONETA.RU.
$integrityCode = $this->decodePostedString('payment_payanyway_integrity_code');
if (('' === $integrityCode) && $this->hasIntegrityCode()) {
$integrityCode = (string)$this->config->get('payment_payanyway_integrity_code');
}
if ('' === $integrityCode) {
$this->errors['integrity_code'] = $this->language->get('error_integrity_code');
}
$this->request->post['payment_payanyway_integrity_code'] = $integrityCode;
$fiscalization = !empty($this->request->post['payment_payanyway_fiscalization']);
$this->request->post['payment_payanyway_fiscalization'] = $fiscalization ? 1 : 0;
$this->request->post['payment_payanyway_test_mode'] = empty($this->request->post['payment_payanyway_test_mode']) ? 0 : 1;
$receiptEmail = $this->decodePostedString('payment_payanyway_receipt_email');
if (($fiscalization || ('' !== $receiptEmail)) && !$this->isValidReceiptEmail($receiptEmail)) {
$this->errors['receipt_email'] = $this->language->get('error_receipt_email');
}
$this->request->post['payment_payanyway_receipt_email'] = $receiptEmail;
if (!in_array($this->request->post['payment_payanyway_payment_method'] ?? '', self::PAYMENT_METHODS, true)) {
$this->request->post['payment_payanyway_payment_method'] = self::DEFAULT_PAYMENT_METHOD;
}
if (!in_array($this->request->post['payment_payanyway_payment_object'] ?? '', self::PAYMENT_OBJECTS, true)) {
$this->request->post['payment_payanyway_payment_object'] = self::DEFAULT_PAYMENT_OBJECT;
}
$canceled = (array)($this->request->post['payment_payanyway_canceled_order_status_ids'] ?? []);
$this->request->post['payment_payanyway_canceled_order_status_ids'] = array_values(array_unique(array_filter(array_map('intval', $canceled), static fn (int $statusId): bool => $statusId > 0)));
if (empty($this->request->post['payment_payanyway_pending_order_status_id'])) {
$this->errors['pending_order_status'] = $this->language->get('error_pending_order_status');
}
if (empty($this->request->post['payment_payanyway_success_order_status_id'])) {
$this->errors['success_order_status'] = $this->language->get('error_success_order_status');
}
if (empty($this->request->post['payment_payanyway_error_order_status_id'])) {
$this->errors['error_order_status'] = $this->language->get('error_error_order_status');
}
$moduleStatuses = [
(int)($this->request->post['payment_payanyway_pending_order_status_id'] ?? 0),
(int)($this->request->post['payment_payanyway_success_order_status_id'] ?? 0),
(int)($this->request->post['payment_payanyway_error_order_status_id'] ?? 0),
];
if ([] !== array_intersect($this->request->post['payment_payanyway_canceled_order_status_ids'], array_diff($moduleStatuses, [0]))) {
$this->errors['canceled_order_status'] = $this->language->get('error_canceled_order_status');
}
$sortOrder = isset($this->request->post['payment_payanyway_sort_order'])
? (int)$this->request->post['payment_payanyway_sort_order']
: self::MODULE_DEFAULT_SORT_ORDER;
$this->request->post['payment_payanyway_sort_order'] = $sortOrder;
$this->request->post['payment_payanyway_cms_name'] = $this->getCmsName();
return empty($this->errors);
}
/** @param array<string, mixed> $data */
private function applyValidationErrors(array &$data): void
{
$data['error_warning'] = $this->errors['warning'] ?? '';
$data['error_account'] = $this->errors['account'] ?? '';
$data['error_integrity_code'] = $this->errors['integrity_code'] ?? '';
$data['error_receipt_email'] = $this->errors['receipt_email'] ?? '';
$data['error_pending_order_status'] = $this->errors['pending_order_status'] ?? '';
$data['error_success_order_status'] = $this->errors['success_order_status'] ?? '';
$data['error_error_order_status'] = $this->errors['error_order_status'] ?? '';
}
/** @return array<int, array{text: string, href: string}> */
private function getBreadcrumbs(): array
{
return [
[
'text' => $this->language->get('text_home'),
'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token']),
],
[
'text' => $this->language->get('text_extension'),
'href' => $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment'),
],
[
'text' => $this->language->get('heading_title'),
'href' => $this->url->link('extension/payanyway/payment/payanyway', 'user_token=' . $this->session->data['user_token']),
],
];
}
private function hasIntegrityCode(): bool
{
return '' !== (string)$this->config->get('payment_payanyway_integrity_code');
}
/** Строка с формы без экранирования OpenCart: htmlspecialchars_decode обращает ENT_COMPAT из system/library/request.php. */
private function decodePostedString(string $name): string
{
$value = $this->request->post[$name] ?? '';
return is_string($value) ? trim(htmlspecialchars_decode($value, ENT_COMPAT)) : '';
}
private function isValidReceiptEmail(string $email): bool
{
return ('' !== $email)
&& (strlen($email) <= self::RECEIPT_EMAIL_MAX_LENGTH)
&& (false !== filter_var($email, FILTER_VALIDATE_EMAIL));
}
private function isFiscalizationEnabled(): bool
{
// Config::get() в OpenCart 4 для отсутствующего ключа возвращает '' — наличие через has().
return !$this->config->has('payment_payanyway_fiscalization')
|| (bool)$this->config->get('payment_payanyway_fiscalization');
}
/**
* @return int[]
*/
private function getCanceledOrderStatusIds(): array
{
if (!$this->config->has('payment_payanyway_canceled_order_status_ids')) {
return self::DEFAULT_CANCELED_ORDER_STATUS_IDS;
}
return array_map('intval', (array)$this->config->get('payment_payanyway_canceled_order_status_ids'));
}
/**
* @param string[] $values
* @return array<string, string>
*/
private function getOptions(array $values, string $labelPrefix): array
{
$result = [];
foreach ($values as $value) {
$result[$value] = $this->language->get($labelPrefix . $value);
}
return $result;
}
/** @return array<string, mixed> */
private function getModuleVatRates(): array
{
$result = array();
foreach (self::AVAILABLE_VAT_RATES as $vat => $value) {
$label = 'entry_' . $vat . '_label';
$result[$value] = $this->language->get($label);
}
return $result;
}
/**
* @return array<string, mixed>
* @throws \Exception
*/
private function getModuleCurrencies(): array
{
$this->load->model('localisation/currency');
$shopCurrencies = $this->model_localisation_currency->getCurrencies();
$result = array();
foreach (self::SUPPORTED_CURRENCIES as $currencyCode) {
$result[$currencyCode] = $shopCurrencies[$currencyCode] ?? $this->getDefaultCurrencyDefinition($currencyCode);
}
return $result;
}
/** @return array<string, mixed> */
private function getDefaultCurrencyDefinition(string $currencyCode): array
{
$definitions = [
'RUB' => [
'currency_id' => 0,
'title' => 'Рубль',
'code' => 'RUB',
'symbol_left' => '',
'symbol_right' => '₽',
'decimal_place' => '2',
'value' => 1.00000000,
'status' => '1',
'date_modified' => date('Y-m-d H:i:s'),
],
];
return $definitions[$currencyCode] ?? [];
}
/**
* @return array<string, mixed>
* @throws \Exception
*/
private function getAvailableOrderStatuses(): array
{
$this->load->model('localisation/order_status');
return $this->model_localisation_order_status->getOrderStatuses();
}
/**
* @return array<string, mixed>
* @throws \Exception
*/
private function getAvailableGeoZones(): array
{
$this->load->model('localisation/geo_zone');
return $this->model_localisation_geo_zone->getGeoZones();
}
private function getCmsName(): string
{
$this->load->language('common/footer');
$textFooter = $this->language->get('text_footer');
$textFooter = is_string($textFooter) ? $textFooter : '';
return $this->detectCmsByPatterns($textFooter);
}
private function detectCmsByPatterns($textFooter): string
{
$patterns = [
'ocStore' => '/ocStore/i',
'Russian OpenCart' => '/opencart-russia/i',
self::CMS_NAME => '/opencart\.com/i'
];
if (!empty($textFooter)) {
foreach ($patterns as $cms => $pattern) {
if (preg_match($pattern, $textFooter)) {
return $cms;
}
}
}
return self::CMS_NAME;
}
private function retrieveValue(string $name): mixed
{
return $this->request->post[$name] ?? $this->config->get($name);
}
private function retrieveValueOrDefault(string $name, string $default): string
{
$value = (string)$this->retrieveValue($name);
return ('' !== $value) ? $value : $default;
}
}