'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') . "

\n"; foreach ($this->errors as $error) { $errorString .= $error . "\n"; } $errorString = substr($errorString, 0, -1) . "

"; $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 $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 */ 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 */ private function getOptions(array $values, string $labelPrefix): array { $result = []; foreach ($values as $value) { $result[$value] = $this->language->get($labelPrefix . $value); } return $result; } /** @return array */ 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 * @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 */ 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 * @throws \Exception */ private function getAvailableOrderStatuses(): array { $this->load->model('localisation/order_status'); return $this->model_localisation_order_status->getOrderStatuses(); } /** * @return array * @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; } }