plugins/payment/classes/yf_payment_api__provider_yandexmoney.class.php

Summary

Maintainability
F
1 wk
Test Coverage
<?php

_class('payment_api__provider_remote');

class yf_payment_api__provider_yandexmoney extends yf_payment_api__provider_remote
{
    public $URL = 'https://money.yandex.ru/quickpay/confirm.xml';
    public $SHOP_ID = null;     // shop id
    public $KEY_PUBLIC = null;     // account id
    public $KEY_PRIVATE = null;     // secret key

    public $HASH_KEY = 'sha1_hash';

    public $fee = 0.5; // PC = 0.5%, AC = 2%

    public $URL_API = 'https://money.yandex.ru/%method';
    public $API_REDIRECT_URI = null;

    public $method_allow = [
        'order' => [
            'payin' => [
                'yandexmoney',
            ],
            'payout' => [
                'yandexmoney_p2p',
            ],
        ],
        'payin' => [
            'yandexmoney' => [
                'title' => 'Яндекс.Деньги',
                'icon' => 'yandexmoney',
                'fee' => 2, // 0.1%
                'currency' => [
                    'RUB' => [
                        'currency_id' => 'RUB',
                        'active' => true,
                    ],
                ],
            ],
        ],
        'api' => [
            'authorize' => [
                'uri' => [
                    '%method' => 'oauth/authorize',
                ],
            ],
            'token' => [
                'uri' => [
                    '%method' => 'oauth/token',
                ],
            ],
            // token revoke
            'revoke' => [
                'is_authorization' => true,
                'request' => [
                    'is_post' => true,
                    'is_http_raw' => true,
                ],
                'uri' => [
                    '%method' => 'api/revoke',
                ],
            ],
            // account-info
            'account-info' => [
                'is_authorization' => true,
                'uri' => [
                    '%method' => 'api/account-info',
                ],
            ],
            // operation-history
            'operation-history' => [
                'is_authorization' => true,
                'uri' => [
                    '%method' => 'api/operation-history',
                ],
            ],
            // operation-details
            'operation-details' => [
                'is_authorization' => true,
                'uri' => [
                    '%method' => 'api/operation-details',
                ],
            ],
            // request-payment
            'request-payment' => [
                'is_authorization' => true,
                'uri' => [
                    '%method' => 'api/request-payment',
                ],
            ],
            'process-payment' => [
                'is_authorization' => true,
                'uri' => [
                    '%method' => 'api/process-payment',
                ],
            ],
        ],
        /**
         * Ограничения на выплаты:
         *   Идентифицированный: 250 000 руб.
         *   Именной:             60 000 руб.
         *   Анонимный:           15 000 руб.
         */
        'payout' => [
            'yandexmoney_p2p' => [
                'title' => 'YandexMoney',
                'icon' => 'yandexmoney',
                'amount' => [
                    'min' => 5,
                    'max' => 200,
                ],
                'is_fee' => true,
                'fee' => [
                    'out' => [
                        'rt' => 0.5,
                        // 'fix' => 10,
                    ],
                ],
                'is_currency' => true,
                'currency' => [
                    'RUB' => [
                        'currency_id' => 'RUB',
                        'active' => true,
                    ],
                ],
                'request_option' => [
                    'pattern_id' => 'p2p',
                ],
                'request_field' => [
                    'pattern_id',
                    'to',
                    'amount',
                    'label',
                    'comment',
                    'message',
                ],
                'field' => [
                    'to',
                ],
                'order' => [
                    'to',
                ],
                'option' => [
                    'to' => 'Номер счета',
                ],
                'option_validation_js' => [
                    'to' => [
                        'type' => 'text',
                        'required' => true,
                        'minlength' => 11,
                        'maxlength' => 26,
                        // 'pattern'   => '^\d+$',
                        'pattern' => '^41001[0-9]{4,19}(?:[1-9]{2})$',
                    ],
                ],
                'option_validation' => [
                    'to' => 'required|length[11,26]|regex:~^41001[0-9]{4,19}(?:[1-9]{2})$~',
                ],
                'option_validation_message' => [
                    'to' => 'обязательное поле от 11 до 26 цифр',
                ],
            ],
        ],
    ];

    public $_api_transform = [
        'title' => 'message',
    ];

    public $_api_transform_reverse = [
        'status' => 'state',
        'contract_amount' => 'amount',
        'payment_id' => 'external_id',
    ];

    public $_options_transform = [
        'amount' => 'sum',
        'title' => 'targets',     // payment title
        'operation_id' => 'label',
    ];

    public $_options_transform_reverse = [
        'sum' => 'amount',
        'targets' => 'title',
        'operation_id' => 'external_id',
        'label' => 'operation_id',
    ];

    public $_status = [
        'success' => 'success',
        'fail' => 'refused',
    ];

    public $_payout_status = [
        'success' => 'success',
        'in_progress' => 'in_progress',
        // refused
        'payment_refused' => 'refused',
        'payee_not_found' => 'refused',
        'authorization_reject' => 'refused',
        'account_blocked' => 'refused',
        'ext_action_required' => 'refused',
        // error
        'error' => 'external error',
        'illegal_params' => 'error',
        'illegal_param_label' => 'error',
        'illegal_param_to' => 'error',
        'illegal_param_amount' => 'error',
        'illegal_param_amount_due' => 'error',
        'illegal_param_comment' => 'error',
        'illegal_param_message' => 'error',
        'illegal_param_expire_period' => 'error',
        'not_enough_funds' => 'error',
        'limit_exceeded' => 'error',
        'contract_not_found' => 'error',
        'money_source_not_available' => 'error',
    ];

    public $_payout_status_message = [
        'success' => 'Платеж проведен.',
        'in_progress' => 'Авторизация платежа не завершена. Повторите запрос спустя некоторое время.',
        // refused
        'payment_refused' => 'Магазин отказал в приеме платежа.',
        'payee_not_found' => 'Указанный счет не существует или не связанный со счетом пользователя.',
        'authorization_reject' => 'В авторизации платежа отказано.',
        'account_blocked' => 'Счет пользователя заблокирован.',
        'ext_action_required' => 'В настоящее время данный тип платежа не может быть проведен.',
        // error
        'external error' => 'Техническая ошибка, повторите вызов операции позднее.',
        'illegal_params' => 'Обязательные параметры платежа отсутствуют или имеют недопустимые значения.',
        'illegal_param_label' => 'Недопустимое значение параметра label.',
        'illegal_param_to' => 'Недопустимое значение параметра счета (to).',
        'illegal_param_amount' => 'Недопустимое значение параметра amount.',
        'illegal_param_amount_due' => 'Недопустимое значение параметра amount_due.',
        'illegal_param_comment' => 'Недопустимое значение параметра comment.',
        'illegal_param_message' => 'Недопустимое значение параметра message.',
        'illegal_param_expire_period' => 'Недопустимое значение параметра expire_period.',
        'not_enough_funds' => 'На счете плательщика недостаточно средств.',
        'limit_exceeded' => 'Превышен один из лимитов на операции.',
        'contract_not_found' => 'Отсутствует созданный (но не подтвержденный) платеж с заданным request_id.',
        'money_source_not_available' => 'Запрошенный метод платежа (money_source) недоступен для данного платежа.',
    ];

    public $currency_default = 'RUB';
    public $currency_allow = [
        'RUB' => [
            'currency_id' => 'RUB',
            'active' => true,
        ],
    ];

    public $service_allow = [
        'Яндекс.Деньги',
    ];

    public $url_result = null;
    public $url_server = null;
    public $url_authorize = null;

    public $provider_name = 'yandexmoney';
    public $provider_id = null;
    public $provider = null;
    public $access_token = null;

    public function _init()
    {
        if ( ! $this->ENABLE) {
            return  null;
        }
        // parent
        parent::_init();
        // class
        $payment_api = &$this->payment_api;
        $provider_name = &$this->provider_name;
        $provider_id = &$this->provider_id;
        $provider = &$this->provider;
        $access_token = &$this->access_token;
        // load api
        require_once __DIR__ . '/payment_provider/yandexmoney/YandexMoney.php';
        $this->api = new YandexMoney($this->KEY_PUBLIC, $this->KEY_PRIVATE);
        $this->url_result = url_user('/api/payment/provider?name=yandexmoney&operation=response');
        $this->url_server = url_user('/api/payment/provider?name=yandexmoney&operation=response&server=true');
        $this->url_authorize = url_user('/api/payment/provider?name=yandexmoney&operation=authorize&server=true');
        // provider options
        list($provider_id, $provider) = $payment_api->get_provider(['name' => $provider_name]);
        $access_token = @$provider['options']['authorize']['access_token'];
        ! is_string($access_token) && $access_token = null;
    }

    public function key($name = 'public', $value = null)
    {
        if ( ! $this->ENABLE) {
            return  null;
        }
        $value = $this->api->key($name, $value);
        return  $value;
    }

    public function key_reset()
    {
        if ( ! $this->ENABLE) {
            return  null;
        }
        $this->key('public', $this->KEY_PUBLIC);
        $this->key('private', $this->KEY_PRIVATE);
    }

    public function signature($options, $request = true)
    {
        if ( ! $this->ENABLE) {
            return  null;
        }
        $result = $this->api->signature($options, $request);
        return  $result;
    }

    public function _form_options($options)
    {
        if ( ! $this->ENABLE) {
            return  null;
        }
        $_ = $options;
        // transform
        foreach ((array) $this->_options_transform as $from => $to) {
            if (isset($_[$from])) {
                $_[$to] = $_[$from];
                unset($_[$from]);
            }
        }
        unset($_['description'], $_['currency']);
        // payment title
        $title = '';
        if (@$_['targets']) {
            switch (true) {
                case defined('SITE_ADVERT_NAME'):
                    $title = SITE_ADVERT_NAME;
                    break;
                case defined('SITE_ADVERT_TITLE'):
                    $title = SITE_ADVERT_TITLE;
                    break;
                case @$_SERVER['SERVER_NAME']:
                    $title = $_SERVER['SERVER_NAME'];
                    break;
            }
            if ( ! empty($title)) {
                $title .= ': ' . $_['targets'];
                $_['formcomment'] = &$title;
                $_['short-dest'] = &$title;
            }
        }
        // default
        $amount = number_format($_['sum'], 2, '.', '');
        if ((int) $amount != (int) $_['sum']) {
            return  null;
        }
        $_['sum'] = $amount;
        if ($this->is_test()) {
            $_['sum'] = '0.03';
        }
        // default fields
        $_['label'] = (@$this->SHOP_ID ?: 'shop id') . ':' . $_['label'];
        $_['receiver'] = $this->key('public');
        $_['quickpay-form'] = 'shop';
        $_['paymentType'] = 'PC';
        $_['need-flo'] = 'false';
        $_['need-email'] = 'false';
        $_['need-phone'] = 'false';
        $_['need-address'] = 'false';
        return  $_;
    }

    public function _url($options, $is_server = false)
    {
        if ( ! $this->ENABLE) {
            return  null;
        }
        is_array($options) && extract($options, EXTR_PREFIX_ALL | EXTR_REFS, '');
        if ($is_server) {
            $url = $_url_server ?: $this->url_server;
        } else {
            $url = $_url_result ?: $this->url_result;
        }
        $result = $url . '&operation_id=' . $_operation_id;
        return  $result;
    }

    public function _form($data, $options = null)
    {
        if ( ! $this->ENABLE) {
            return  null;
        }
        if (empty($data)) {
            return  null;
        }
        $_ = &$options;
        // START DUMP
        $payment_api = $this->payment_api;
        $payment_api->dump(['name' => 'YandexMoney', 'operation_id' => @(int) $_['data']['operation_id']]);
        $is_array = (bool) $_['is_array'];
        $form_options = $this->_form_options($data);
        // DUMP
        $payment_api->dump(['var' => $form_options]);
        $url = &$this->URL;
        $result = [];
        if ($is_array) {
            $result['url'] = $url;
        } else {
            $result[] = '<form id="_js_provider_yandexmoney_form" method="post" accept-charset="utf-8" action="' . $url . '" class="display: none;">';
        }
        foreach ((array) $form_options as $key => $value) {
            if ($is_array) {
                $result['data'][$key] = $value;
            } else {
                $result[] = sprintf('<input type="hidden" name="%s" value="%s" />', $key, $value);
            }
        }
        if ( ! $is_array) {
            $result[] = '</form>';
            $result = implode(PHP_EOL, $result);
        }
        return  $result;
    }

    public function _api_response()
    {
        if ( ! $this->ENABLE) {
            return  null;
        }
        $payment_api = $this->payment_api;
        // var
        $test_mode = &$this->TEST_MODE;
        $is_server = ! empty($_GET['server']);
        $result = null;
        //* // TEST
        if ($this->is_test()) {
            $operation_id = (int) $_GET['operation_id'];
            $this->key('private', '01234567890ABCDEF01234567890');
            $_POST = [
                'notification_type' => 'p2p-incoming',
                'operation_id' => '1234567',
                'amount' => '300.00',
                'currency' => '643',
                'datetime' => '2011-07-01T09:00:00.000+04:00',
                'sender' => '41001XXXXXXXX',
                'codepro' => 'false',
                'sha1_hash' => '090a8e7ebb6982a7ad76f4c0f0fa5665d741aafa',
                'withdraw_amount' => '100.00',
                // shop_id:operation_id may be exists
                'label' => $this->SHOP_ID . ':' . $operation_id,
            ];
        } // */
        // check shop_id, operation
        @list($shop_id, $operation_id) = explode(':', $_POST['label']);
        $operation_id = (int) $operation_id;
        // START DUMP
        $payment_api->dump(['name' => 'YandexMoney', 'operation_id' => $operation_id]);
        if ($shop_id != $this->SHOP_ID) {
            $result = [
                'status' => false,
                'status_message' => 'Не верный идентификатор магазина',
            ];
            // DUMP
            $payment_api->dump(['var' => $result]);
            return  $result;
        }
        // response
        $response = $_POST;
        // operation_id
        if (empty($operation_id)) {
            $result = [
                'status' => false,
                'status_message' => 'Не определен код операции',
            ];
            // DUMP
            $payment_api->dump(['var' => $result]);
            return  $result;
        }
        // signature
        $signature = @$response[$this->HASH_KEY];
        //* // TEST
        if ($this->is_test()) {
            unset($response['label']);
        } // */
        $_signature = $this->signature($response, false);
        $is_signature_ok = $signature == $_signature;
        if ( ! $is_signature_ok) {
            $result = [
                'status' => false,
                'status_message' => 'Неверная подпись',
            ];
            // DUMP
            $payment_api->dump(['var' => $result]);
            return  $result;
        }
        // check status
        $state = 'fail';
        // check payin
        $is_codepro = @$response['codepro'] == 'true';
        $is_unaccepted = @$response['unaccepted'] == 'true';
        $is_payin = ! ($is_codepro || $is_unaccepted);
        if ($is_server && $is_signature_ok && $is_payin) {
            $state = 'success';
        }
        list($status_name, $status_message) = $this->_state($state);
        $status = $status_name == 'success';
        // get response
        $_response = $this->_response_parse($response);
        // check operation data
        $operation = $payment_api->operation(['operation_id' => $operation_id]);
        $_operation_id = @$operation['operation_id'];
        $amount = @$_response['amount'];
        // $_amount       = @$operation[ 'amount'       ];
        $is_operation_ok =
            $operation_id == (int) $_operation_id
            // && ( $amount == $_amount || $this->is_test() )
;
        if ( ! $is_operation_ok) {
            $result = [
                'status' => false,
                'status_message' => 'Неверные данные запроса',
            ];
            // DUMP
            $payment_api->dump(['var' => $result]);
            return  $result;
        }
        $_response['operation_id'] = $operation_id;
        $_response['shop_id'] = $shop_id;
        // update account, operation data
        $result = $this->_api_deposition([
            'provider_name' => 'yandexmoney',
            'response' => $_response,
            'status_name' => $status_name,
            'status_message' => $status_message,
        ]);
        return  $result;
    }

    public function _response_parse($response)
    {
        if ( ! $this->ENABLE) {
            return  null;
        }
        $_ = $response;
        // transform
        foreach ((array) $this->_options_transform_reverse as $from => $to) {
            if (isset($_[$from])) {
                $_[$to] = $_[$from];
                unset($_[$from]);
            }
        }
        return  $_;
    }

    // *********** API

    public function api_token_revoke($options = null)
    {
        if ( ! $this->ENABLE) {
            return  null;
        }
        // var
        $payment_api = &$this->payment_api;
        $provider_name = &$this->provider_name;
        $provider_id = &$this->provider_id;
        // import options
        is_array($options) && extract($options, EXTR_PREFIX_ALL | EXTR_REFS, '');
        // request
        $request = [
            'method_id' => 'revoke',
        ];
        $result = $this->api_request($request);
        $http_code = (int) $result['http_code'];
        $status = null;
        $status_message = null;
        switch ($http_code) {
            case 200:
                $status = true;
                $status_message = 'Токен успешно отозван';
                break;
            case 401:
                $status = false;
                $status_message = 'Указанный токен не существует';
                break;
            case 400:
            default:
                $status_message = 'Неверный запрос';
                break;
        }
        if ($status !== null) {
            $r = $payment_api->provider_update([
                'provider_id' => $provider_id,
                'options' => ['redirect_uri' => null, 'authorize' => null],
            ], ['is_replace' => true]);
            if ( ! @$r['status']) {
                return  $r;
            }
        }
        $result = [
            'status' => $status,
            'status_message' => $status_message,
        ];
        return  $result;
    }

    public function _get_api_redirect_uri($options = null)
    {
        // import options
        is_array($options) && extract($options, EXTR_PREFIX_ALL | EXTR_REFS, '');
        // var
        $result = $this->API_REDIRECT_URI;
        $_redirect_hash && $result .= '&redirect_hash=' . $_redirect_hash;
        return  $result;
    }

    public function authorize_request($options = null)
    {
        if ( ! $this->ENABLE) {
            return  null;
        }
        // var
        $payment_api = &$this->payment_api;
        $provider_name = &$this->provider_name;
        $provider_id = &$this->provider_id;
        // import options
        is_array($options) && extract($options, EXTR_PREFIX_ALL | EXTR_REFS, '');
        $request_options = [];
        // redirect uri
        $redirect_hash = null;
        if (@$_redirect_uri) {
            $redirect_hash = substr(md5($_redirect_uri . time()), 0, 8);
            $r = $payment_api->provider_update([
                'provider_id' => $provider_id,
                'options' => ['redirect_uri' => [
                    $redirect_hash => [
                        'value' => $_redirect_uri,
                        'datetime' => $payment_api->sql_datetime(),
                    ],
                ]],
            ], ['is_replace' => true]);
            if ( ! @$r['status']) {
                return  $r;
            }
        }
        // request options
        $API_REDIRECT_URI = $this->_get_api_redirect_uri([
            'redirect_hash' => $redirect_hash,
        ]);
        $default = [
            'client_id' => $this->API_KEY_PUBLIC,
            'client_secret' => $this->API_KEY_PRIVATE,
            'redirect_uri' => $API_REDIRECT_URI,
            'response_type' => 'code',
            'instance_name' => 'admin',
            'scope' => 'account-info operation-history operation-details incoming-transfers payment-p2p money-source("wallet")',
        ];
        $request_options = $default;
        $method_id = 'authorize';
        is_array($_option) && $request_options = array_replace_recursive($request_options, $_option);
        // method
        $method = $this->api_method([
            'type' => 'api',
            'method_id' => $method_id,
        ]);
        if (empty($method)) {
            $result = [
                'status' => false,
                'status_message' => 'Метод запроса не найден',
            ];
            return  $result;
        }
        // url
        $object = $this->api_url($method, $options);
        if (isset($object['status']) && $object['status'] === false) {
            return  $object;
        }
        $url = $object;
        // form
        $result = [];
        $result[] = '<form id="_js_provider_yandexmoney_authorize_form" method="post" accept-charset="utf-8" action="' . $url . '" class="display: none;">';
        foreach ($request_options as $key => $value) {
            $result[] = sprintf('<input type="hidden" name="%s" value="%s" />', $key, htmlentities($value, ENT_COMPAT | ENT_HTML5, 'UTF-8', false));
        }
        $result[] = '<script>document.getElementById(\'_js_provider_yandexmoney_authorize_form\').submit();</script>';
        $result[] = '</form>';
        // DEBUG
        // var_dump( $result ); exit;
        $result = implode(PHP_EOL, $result);
        echo $result;
        exit;
        // return( $result );
    }

    public function _api_authorize()
    {
        if ( ! $this->ENABLE) {
            return  null;
        }
        // var
        $payment_api = &$this->payment_api;
        $provider_name = &$this->provider_name;
        $provider_id = &$this->provider_id;
        $provider = &$this->provider;
        $is_server = ! empty($_GET['server']);
        $redirect_uri = null;
        // START DUMP
        $payment_api->dump(['name' => 'YandexMoney-authorize']);
        // token
        $code = &$_GET['code'];
        $redirect_hash = &$_GET['redirect_hash'];
        $redirect_uri = &$provider['options']['redirect_uri'][$redirect_hash]['value'];
        // DUMP
        $payment_api->dump(['var' => [
            'code' => $code,
            'redirect_hash' => $redirect_hash,
        ]]);
        if ($code && $redirect_hash) {
            $API_REDIRECT_URI = $this->_get_api_redirect_uri([
                'redirect_hash' => $redirect_hash,
            ]);
            $request = [
                'method_id' => 'token',
                'option' => [
                    'client_id' => $this->API_KEY_PUBLIC,
                    'client_secret' => $this->API_KEY_PRIVATE,
                    'redirect_uri' => $API_REDIRECT_URI,
                    'grant_type' => 'authorization_code',
                    'code' => $code,
                ],
            ];
            // DUMP
            $payment_api->dump(['var' => [
                'token request' => $request,
            ]]);
            list($status, $response) = $this->api_request($request);
            // DUMP
            $payment_api->dump(['var' => [
                'token response status' => $status,
                'token response' => $response,
            ]]);
            // store access_token
            if (@$status && @$response['access_token']) {
                $access_token = &$this->access_token;
                $access_token = $response['access_token'];
                $provider_id = &$this->provider_id;
                $r = $payment_api->provider_update([
                    'provider_id' => $provider_id,
                    'options' => [
                        'authorize' => [
                            'access_token' => $access_token,
                            'datetime' => $payment_api->sql_datetime(),
                        ],
                        'redirect_uri' => [$redirect_hash => null],
                    ],
                ], ['is_replace' => true]);
                if ( ! @$r['status']) {
                    return  $r;
                }
            }
        } else {
            // DUMP
            $payment_api->dump(['var' => [
                'error' => 'No code or redirect_hash',
            ]]);
        }
        // redirect
        $redirect_uri = $redirect_uri ?: '/payments';
        return  _class('api')->_redirect($redirect_uri);
    }

    public function is_authorization()
    {
        $result = (bool) $this->access_token;
        return  $result;
    }

    public function api_authorization($method = null)
    {
        $result = $this->is_authorization();
        if ( ! $result) {
            $result = [
                'status' => false,
                'status_message' => 'Требуется авторизация YandexMoney',
            ];
            return  $result;
        }
        $result = [
            'status' => true,
            'status_message' => 'Авторизация имеется',
        ];
        return  $result;
    }

    public function api_request($options = null)
    {
        if ( ! $this->ENABLE) {
            return  null;
        }
        // import options
        if (is_string($options)) {
            $_method_id = $options;
        }
        is_array($options) && extract($options, EXTR_PREFIX_ALL | EXTR_REFS, '');
        // method
        $method = $this->api_method([
            'type' => 'api',
            'method_id' => @$_method_id,
        ]);
        if (empty($method)) {
            $result = [
                'status' => false,
                'status_message' => 'Метод запроса не найден',
            ];
            return  $result;
        }
        // method handler
        if ( ! empty($method['is_handler'])) {
            $handler = 'api_request__' . $method['is_handler'];
            if ( ! method_exists($this, $handler)) {
                $result = [
                    'status' => false,
                    'status_message' => 'Опработчик метода запроса не найден',
                ];
                return  $result;
            }
            $result = $this->{ $handler }($options);
            return  $result;
        }
        // request
        $request = [];
        is_array(@$_option) && $request = $_option;
        // DEBUG
        // var_dump( $url, $request, $request_option );
        // exit;
        // add options
        is_array($method['option']) && $request = array_replace_recursive(
            $method['option'],
            $request
        );
        // url
        $object = $this->api_url($method, $options);
        if (isset($object['status']) && $object['status'] === false) {
            return  $object;
        }
        $url = $object;
        // request options
        $request_option = [];
        @$_is_debug && $request_option['is_debug'] = true;
        if (@$method['is_authorization']) {
            $result = $this->api_authorization($request_option);
            $request_option['access_token'] = $this->access_token;
            if ( ! @$result['status']) {
                return  $result;
            }
        }
        // header
        is_array($method['header']) && $request_option = array_replace_recursive($request_option, ['header' => $method['header']]);
        is_array($method['request']) && $request_option = array_replace_recursive($request_option, $method['request']);
        is_array($_header) && $request_option = array_replace_recursive($request_option, ['header' => $_header]);
        // request
        // DEBUG
        // var_dump( $url, $request, $request_option );
        // exit;
        $result = $this->_api_request($url, $request, $request_option);
        // var_dump( $result );
        // exit;
        return  $result;
    }

    public function api_payout($options = null)
    {
        if ( ! $this->ENABLE) {
            return  null;
        }
        // import options
        is_array($options) && extract($options, EXTR_PREFIX_ALL | EXTR_REFS, '');
        // method
        $method = $this->api_method([
            'type' => 'payout',
            'method_id' => @$_method_id,
        ]);
        if (empty($method)) {
            $result = [
                'status' => false,
                'status_message' => 'Метод запроса не найден',
            ];
            return  $result;
        }
        $payment_api = &$this->payment_api;
        // operation_id
        $_operation_id = (int) $_operation_id;
        $operation_id = $_operation_id;
        if (empty($_operation_id)) {
            $result = [
                'status' => false,
                'status_message' => 'Не определен код операции',
            ];
            return  $result;
        }
        // currency_id
        $currency_id = $this->get_currency_payout($options);
        if (empty($currency_id)) {
            $result = [
                'status' => false,
                'status_message' => 'Неизвестная валюта',
            ];
            return  $result;
        }
        // amount min/max
        $result = $this->amount_limit([
            'amount' => $_amount,
            'currency_id' => $currency_id,
            'method' => $method,
        ]);
        if ( ! @$result['status']) {
            return  $result;
        }
        // amount currency conversion
        $result = $this->currency_conversion_payout([
            'options' => $options,
            'method' => $method,
            'amount' => $_amount,
        ]);
        if (empty($result['status'])) {
            return  $result;
        }
        $amount_currency = $result['amount_currency'];
        $amount_currency_total = $result['amount_currency_total'];
        $currency_id = $result['currency_id'];
        // default
        $amount = @$method['is_fee'] ? $amount_currency_total : $amount_currency;
        $amount = $payment_api->_number_api($amount, 2);
        // request
        $request = [];
        @$method['request_option'] && $request = $method['request_option'];
        // add common fields
        $request['label'] = (@$this->SHOP_ID ?: 'shop id') . ':' . $operation_id;
        // amount
        $this->is_test() && $amount = '0.01';
        $request['amount'] = $amount;
        // account
        @$_to && $request['to'] = $_to;
        // test
        if ($this->is_test()) {
            $request += [
                'test_payment' => 'true',
                'test_result' => @$_test_result1 ?: 'success',
                // 'test_result'  => 'account_blocked',
                // 'test_result'  => 'illegal_params',
            ];
        }
        // title
        @$_title && $request['title'] = $_title;
        @$_operation_title && $request['title'] = $_operation_title;
        // transform
        $this->option_transform([
            'option' => &$request,
            'transform' => $this->_api_transform,
        ]);
        // add fields
        $request['comment'] = &$request['message'];
        foreach ($method['field'] as $key) {
            $value = &$request[$key];
            if ( ! isset($value)) {
                $result = [
                    'status' => false,
                    'status_message' => 'Отсутствуют данные запроса: ' . $key,
                ];
                return  $result;
            }
        }
        // DEBUG
        // var_dump( $options,$request ); exit;
        // START DUMP
        $payment_api->dump(['name' => 'YandexMoney', 'operation_id' => $operation_id,
            'var' => ['request' => $request],
        ]);
        // update processing
        $sql_datetime = $payment_api->sql_datetime();
        $operation_options = [
            'processing' => [[
                'provider_name' => 'yandexmoney',
                'datetime' => $sql_datetime,
            ]],
        ];
        $operation_update_data = [
            'operation_id' => $operation_id,
            'datetime_update' => $sql_datetime,
            'options' => $operation_options,
        ];
        $payment_api->operation_update($operation_update_data);
        // request options
        $request_option = [
            'method_id' => 'request-payment',
            'option' => $request,
        ];
        @$_is_debug && $request_option['is_debug'] = true;
        // DEBUG
        // var_dump( $request_option );
        $result = $this->api_request($request_option);
        // DEBUG
        // var_dump( $result );
        // DUMP
        $payment_api->dump(['var' => ['response' => $result]]);
        if (@$result['status'] === false) {
            return  $result;
        }
        if ( ! @$result) {
            $result = [
                'status' => false,
                'status_message' => 'Невозможно отправить запрос',
            ];
            return  $result;
        }
        @list($status, $response) = $result;
        // DEBUG
        /*
        $this->is_test() && $status = true && $response = array (
            'status'                   => 'success',
            'contract'                 => 'The generated test outgoing money transfer to 410012771676199, amount 0.01',
            'balance'                  => 958.4,
            'request_id'               => 'test-p2p',
            'recipient_account_type'   => 'personal',
            'recipient_account_status' => 'anonymous',
            'test_payment'             => 'true',
            'contract_amount'          => 0.01,
            'money_source'             => array(
                'wallet' => array(
                    'allowed' => true,
                ),
            ),
            'recipient_identified'     => false,
        ); //*/
        if ( ! @$response) {
            $result = [
                'status' => false,
                'status_message' => 'Невозможно декодировать ответ: ' . var_export($response, true),
            ];
            return  $result;
        }
        // transform reverse
        foreach ($this->_api_transform_reverse as $from => $to) {
            if ($from != $to && isset($response[$from])) {
                $response[$to] = $response[$from];
                unset($response[$from]);
            }
        }
        // result
        list($request_status, $state, $result) = $this->_payout_status_handler($response);
        // DEBUG
        // var_dump( $request_status, $state, $result ); exit;
        // request
        $request_id = @$response['request_id'];
        if ($request_status && $state == 'success') {
            if ( ! $request_id) {
                $result = [
                    'status' => 'error',
                    'status_message' => 'Неверный ответ: отсутствует request_id',
                ];
                return  $result;
            }
            $request_option = ['request_id' => $request_id] + $options;
            list($request_status, $state, $result) = $this->_payout_process($request_option);
            // DEBUG
                // var_dump( $request_status, $state, $result ); exit;
        }
        if ( ! $request_status) {
            return  $result;
        }
        $status_name = &$result['status'];
        $status_message = &$result['status_message'];
        // DEBUG
        // var_dump( $request_status, $state, $result );
        // exit;
        // update account, operation data
        $payment_type = 'payment';
        $operation_data = [
            'operation_id' => $operation_id,
            'provider_force' => @$_provider_force,
            'provider_name' => 'yandexmoney',
            'state' => $state,
            'status_name' => $status_name,
            'status_message' => $status_message,
            'payment_type' => $payment_type,
            'response' => $response,
        ];
        // DEBUG
        // var_dump( $operation_data ); exit;
        // DUMP
        $payment_api->dump(['var' => ['payment_type' => $payment_type, 'update operation' => $operation_data]]);
        $result = $this->{ '_api_' . $payment_type }($operation_data);
        // DUMP
        $payment_api->dump(['var' => ['update result' => $result]]);
        return  $result;
    }

    public function _payout_process($options = null)
    {
        if ( ! $this->ENABLE) {
            return  null;
        }
        // import options
        is_array($options) && extract($options, EXTR_PREFIX_ALL | EXTR_REFS, '');
        if ( ! @$_request_id) {
            $result = [
                'status' => 'processing',
                'status_message' => 'Ошибка: отсутствует request_id',
            ];
            return  [null, $result];
        }
        $payment_api = &$this->payment_api;
        $request = [
            'request_id' => $_request_id,
        ];
        // test
        if ($this->is_test()) {
            $request += [
                'test_payment' => 'true',
                'test_result' => @$_test_result2 ?: 'success',
                // 'test_result'  => 'payment_refused',
                // 'test_result'  => 'not_enough_funds',
            ];
        }
        // process
        $request_option = [
            'method_id' => 'process-payment',
            'option' => $request,
        ];
        @$_is_debug && $request_option['is_debug'] = true;
        // DEBUG
        // var_dump( 'process:', $request_option );
        // DUMP
        $payment_api->dump(['var' => ['process request' => $request_option]]);
        $result = $this->api_request($request_option);
        // DEBUG
        // var_dump( 'process:', $result ); exit;
        // DUMP
        $payment_api->dump(['var' => ['process response' => $result]]);
        if (@$result['status'] === false) {
            return  [null, $result];
        }
        if ( ! @$result) {
            $result = [
                'status' => false,
                'status_message' => 'Невозможно отправить запрос',
            ];
            return  [null, $result];
        }
        @list($status, $response) = $result;
        if ( ! @$response) {
            $result = [
                'status' => false,
                'status_message' => 'Невозможно декодировать ответ: ' . var_export($response, true),
            ];
            return  [null, $result];
        }
        // transform reverse
        foreach ($this->_api_transform_reverse as $from => $to) {
            if ($from != $to && isset($response[$from])) {
                $response[$to] = $response[$from];
                unset($response[$from]);
            }
        }
        // DEBUG state
        // $response[ 'state' ] = 'in_progress';
        // result
        $result = $this->_payout_status_handler($response);
        return  $result;
    }

    public function _payout_status_handler($options = null)
    {
        if ( ! $this->ENABLE) {
            return  null;
        }
        // import options
        is_array($options) && extract($options, EXTR_PREFIX_ALL | EXTR_REFS, '');
        $request_status = false;
        $status_name = false;
        $status_message = null;
        $result = [
            'status' => &$status_name,
            'status_message' => &$status_message,
        ];
        $state = &$_state;
        $error = &$_error;
        // DEBUG state
        // $state = 'refused'; $error = 'error1';
        switch ($state) {
            // success
            case 'success':
                $status = 'success';
                $status_message = 'Выполнено';
                $request_status = true;
                break;
            // in_progress
            case 'in_progress':
                $status = 'in_progress';
                $status_message = 'В процессе';
                $request_status = true;
                break;
            // error
            case 'refused':
                $status = $error;
                $status_message = 'Отказано';
                $request_status = true;
                break;
            default:
                $status = 'unknown';
                $status_message = 'Ошибка: ' . $state;
                break;
        }
        @$status_message && $_message = $status_message;
        // check status
        if ($status != 'unknown') {
            list($status_name, $status_message) = $this->_state(
                $status,
                $this->_payout_status,
                $this->_payout_status_message
            );
            if ( ! $status_name) {
                $status = 'error';
                list($status_name, $status_message) = $this->_state(
                    $status,
                    $this->_payout_status,
                    $this->_payout_status_message
                );
            }
        }
        $status_name == 'error' || $status_name == 'external error'
            && $request_status = false;
        // add error_description
        $status_name == 'error' && $status == 'unknown' && $_error_description
            && $status_message .= ' (' . $_error_description . ')';
        // result
        $state = $status_name ?: $status;
        $status_message = @$status_message ?: $state;
        return  [$request_status, $state, $result];
    }
}