module_title = _t('bills', 'Monetization'); } /** * List of registered payment systems * @param string|null $key to get particular system * @return \bff\contracts\PaymentProvider[] | \bff\contracts\PaymentProvider | null */ public function paymentProvidersList(?string $key = null) { return Providers::get('bills.payment.providers.list', PaymentProvider::class, $key); } /** * Получить объект реализующий шлюз с системой оплаты по ключу * @param string $key ключ системы оплаты объект реализующий шлюз с системой оплаты * @return \bff\contracts\PaymentProvider|null */ public function paymentProvider(string $key): ?PaymentProvider { if (! $key) { return null; } return $this->paymentProvidersList($key); } /** * Available pay ways list * @param bool $balanceUse включить оплату с баланса пользователя * @param array $opts ['enabledOnly'=>true] - только включенные * @return array */ public function getPayWaysList(bool $balanceUse = false, array $opts = []) { $opts = $this->defaults($opts, [ 'enabledOnly' => true, 'promotePage' => false, ]); $path = $this->app->path('svc', 'images'); $url = $this->app->url('svc', 'images'); $settings = config::get(static::PAYWAYS_CONFIG, [], TYPE_ARRAY); foreach ($settings as $k => &$v) { do { if (empty($v['key'])) { break; } $key = $v['key']; $psystem = $this->paymentProvider($key); if (! $psystem) { break; } $ways = $psystem->payWays(); if (empty($ways)) { if (! empty($v['way'])) { break; } } else { if (empty($v['way']) || ! isset($ways[ $v['way'] ])) { break; } } if (! empty($v['logo_file'])) { $v['logo_file'] = str_replace('{path_base}', $this->app->basePath(), $v['logo_file']); $v['logo_url'] = str_replace($path, $url, $v['logo_file']); } else { $v['logo_url'] = $psystem->payLogoUrl(); } $v['logo_desktop'] = $v['logo_url']; $v['logo_mobile'] = $v['logo_url']; $v['title_ps'] = $psystem->providerTitle(); continue 2; } while (false); unset($settings[$k]); } unset($v); $result = []; if ($balanceUse) { $result['balance'] = [ 'key' => 'balance', 'title' => _t('bills', 'Account [name]', ['name' => Site::title('bills.paysystems.balance')]), 'way' => '', 'currency' => Currency::default('keyword'), 'currency_id' => Currency::id(), 'enabled' => true, 'logo_url' => Site::logoURL('header'), 'logo_desktop' => Site::logoURL('header'), 'logo_mobile' => Site::logoURL('header'), ]; } # Исключаем выключенные if ($opts['enabledOnly']) { foreach ($settings as $k => $v) { if (empty($v['enabled'])) { unset($settings[$k]); } } } foreach ($settings as $k => $v) { $result[ $k ] = $v; } return $result; } /** * Add pay way * @param PaymentProvider $system * @param string|null $way * @return void */ public function addPayWay(PaymentProvider $system, ?string $way = null, array $opts = []) { $opts = $this->defaults([ 'enabled' => 1, ], $opts); $systems = $this->getPayWaysList(false, ['enabledOnly' => false]); $key = $system->providerKey(); foreach ($systems as $v) { if ($v['key'] == $key) { return; } } $currencies = Currency::list(false); $currencies = func::array_transparent($currencies, 'keyword', true); $ways = $system->payWays(); $add = function ($cur, $way) use (&$systems, &$system, &$ways, &$currencies, &$opts) { $cur = mb_strtolower($cur); if (! isset($currencies[$cur]['id'])) { return; } do { $hash = func::generatorLetters(10); } while (isset($systems[$hash])); $systems[$hash] = [ 'key' => $system->providerKey(), 'title' => $system->providerTitle() . (isset($ways[ $way ]['title']) ? ' / ' . $ways[ $way ]['title'] : ''), 'way' => $way, 'currency' => $cur, 'currency_id' => $currencies[$cur]['id'], 'enabled' => $opts['enabled'] ?? 0, ]; }; $default = mb_strtolower($system->payDefaultCurrency()); if (! empty($ways)) { $w = false; $cur = $default; if ($way && isset($ways[$way])) { if (! empty($ways[$way]['currency'])) { $cur = mb_strtolower($ways[$way]['currency']); } if (isset($currencies[$cur])) { $w = $ways[$way]; } } if (! $w) { foreach ($ways as $k => $v) { $cur = $default; if (! empty($v['currency'])) { $cur = mb_strtolower($v['currency']); } if (isset($currencies[$cur])) { $w = $k; break; } } } if ($w) { $add($cur, $w); } } else { if (isset($currencies[$default])) { $add($default, ''); } } config::save(static::PAYWAYS_CONFIG, $systems); config::save(static::PAYWAYS_CONFIG_MODIFIED, time()); } /** * Создание счета, тип: пополнение счета * @param int $userID ID пользователя * @param float $userBalance текущий баланс пользователя * @param float $amount сумма счета (виртуальная) * @param float $money сумма счета (оплачиваемая) * @param int $currencyID ID валюты * @param int $statusID статус счета: Bills::STATUS_ * @param string $paySystem система оплаты * @param string $paySystemWay способ оплаты в системе * @param string $description описание * @param string $serviceGroup группа оплачиваемой услуги или '' * @param string $serviceKey ключ оплачиваемой услуги или '' * @param int $itemID ID записи (для которой активируется услуга) или 0 * @param array $serviceSettings дополнительные параметры услуги/нескольких услуг * @return int ID счета */ public function createBill_InPay( $userID, $userBalance, $amount, $money, $currencyID, $statusID, $paySystem, $paySystemWay = '', $description = '', $serviceGroup = '', $serviceKey = '', $itemID = 0, array $serviceSettings = [] ) { $billID = $this->model->billSave(0, [ 'user_id' => $userID, 'user_balance' => $userBalance, 'psystem' => $paySystem, 'psystem_way' => $paySystemWay, 'type' => self::TYPE_IN_PAY, 'status' => $statusID, 'amount' => $amount, 'money' => $money, 'currency_id' => $currencyID, 'description' => $description, 'service_group' => $serviceGroup, 'service_key' => $serviceKey, 'service_settings' => $serviceSettings, 'item_id' => $itemID, ]); if (empty($billID)) { $this->log(strtr('Неудалось создать счет: пополнение счета пользователя #[user] на сумму [amount]', [ '[user]' => $userID, '[amount]' => $amount, ])); $billID = 0; } return $billID; } /** * Создание счета, тип: активация услуги * @param string $serviceGroup группа оплачиваемой услуги или '' * @param string $serviceKey ключ оплачиваемой услуги или '' * @param int $itemID ID записи (для которой активируется услуга) или 0 * @param int $userID ID пользователя * @param float|int $userBalance текущий баланс пользователя * @param float $amount сумма счета (виртуальная) * @param float $money сумма счета (оплачиваемая) * @param int $statusID статус счета: Bills::STATUS_ * @param string $description описание счета * @param array $serviceSettings дополнительные параметры услуги/нескольких услуг * @return int ID счета */ public function createBill_OutService( $serviceGroup, $serviceKey, $itemID, $userID, $userBalance, $amount, $money, $statusID, $description = '', array $serviceSettings = [] ) { $aData = [ 'user_id' => $userID, 'user_balance' => $userBalance, 'type' => self::TYPE_OUT_SERVICE, 'status' => $statusID, 'amount' => $amount, 'money' => $money, 'description' => $description, 'service_group' => $serviceGroup, 'service_key' => $serviceKey, 'service_settings' => $serviceSettings, 'item_id' => $itemID, ]; if ($statusID === self::STATUS_COMPLETED) { # в случае если счет со статусом "завершен" # помечаем дату оплаты счета текущим временем $aData['payed'] = $this->db->now(); } $billID = $this->model->billSave(0, $aData); if (empty($billID)) { $this->log(strtr('Неудалось создать счет: активация услуги #[svc], пользователь #[user], объект #[item]', [ '[user]' => $userID, '[svc]' => $serviceGroup . ':' . $serviceKey, '[item]' => $itemID, ])); $billID = 0; } return $billID; } /** * Создание счета, тип: списание со счета администратором * @param int $userID ID пользователя * @param float $userBalance текущий баланс пользователя * @param float $amount сумма счета (виртуальная) * @param string $description описание * @return int ID счета */ public function createBill_OutAdmin($userID, $userBalance, $amount, $description = '') { $billID = $this->model->billSave(0, [ 'user_id' => $userID, 'user_balance' => $userBalance, 'type' => self::TYPE_OUT_ADMIN, 'status' => self::STATUS_COMPLETED, 'amount' => $amount, 'currency_id' => Currency::id(), 'description' => $description, 'payed' => $this->db->now(), ]); if (empty($billID)) { $this->log(strtr('Неудалось списать со счета пользователя #[user], сумму [amount]', [ '[user]' => $userID, '[amount]' => $amount, ])); $billID = 0; } return $billID; } /** * Создание счета, тип: подарок * @param int $userID ID пользователя * @param float $userBalance текущий баланс пользователя * @param float $amount сумма счета (виртуальная) * @param string $description описание * @return int ID счета */ public function createBill_InGift($userID, $userBalance, $amount, $description = '') { $billID = $this->model->billSave(0, [ 'user_id' => $userID, 'user_balance' => $userBalance, 'type' => self::TYPE_IN_GIFT, 'status' => self::STATUS_COMPLETED, 'amount' => $amount, 'currency_id' => Currency::id(), 'description' => $description, 'payed' => $this->db->now(), ]); if (empty($billID)) { $this->log(strtr('Неудалось создать счет: подарок, пользователь #[user], сумма [amount]', [ '[user]' => $userID, '[amount]' => $amount, ])); $billID = 0; } return $billID; } /** * Закрываем счет * @param int $billID ID счета * @param bool $markPayed помечаем дату оплаты счета * @param int|array|bool $userBalance : * - int|float - текущая сумма на счету пользователя * - array - ['user_id'=>ID пользователя,'amount'=>сумма (виртуальная),'add'=>true - добавить, false - вычесть] * - FALSE - не обновлять информацию о счете пользователя * @param mixed|array|string $details доп. детали счета * @return bool */ public function completeBill($billID, $markPayed = false, $userBalance = false, $details = false) { $update = ['status' => self::STATUS_COMPLETED]; # помечаем дату оплаты счета if ($markPayed) { $update['payed'] = $this->db->now(); } # помечаем текущий баланс пользователя if (!empty($userBalance)) { if (is_array($userBalance)) { if (!empty($userBalance['user_id'])) { $balance = Users::model()->userBalance($userBalance['user_id']); if ($userBalance['add']) { # прибавляем к балансу $update['user_balance'] = $balance + $userBalance['amount']; } else { # снимаем с баланса $update['user_balance'] = $balance - $userBalance['amount']; } } } else { $update['user_balance'] = $userBalance; } } # помечаем доп.детали if ($details !== false) { $update['details'] = (is_array($details) ? serialize($details) : $details); } return $this->model->billSave($billID, $update); } /** * Обновление состояния счета пользователя * @param int $userID ID пользователя * @param int|float $amount сумма (виртуальная) * @param bool $increment true - добавить к счету, false - вычесть из счета * @return bool */ public function updateUserBalance($userID, $amount, $increment) { if (!$userID) { return true; } $action = ($increment ? '+' : '-'); $amount = number_format(floatval($amount), 4, '.', ''); $success = Users::model()->userSave($userID, ["balance = balance $action $amount"]); if (!$success) { $this->log(strtr('Неудалось обновить баланс пользователя #[user], сумма [amount]', [ '[user]' => $userID, '[amount]' => $action . ' ' . $amount, ])); return false; } else { if (User::isCurrent($userID)) { User::refresh(); } } return true; } /** * Формирование формы запроса к системе оплаты * @param string $key ключ системы оплаты * @param string $way способ оплаты (в системе оплаты) * @param int $billID id счета * @param float $amount сумма (оплачиваемая) * @param array $extra доп. информация * @return string */ public function buildPayRequestForm(string $key, string $way, int $billID, float $amount, array $extra = []) { $paySystem = $this->paymentProvider($key); if (! $paySystem) { return ''; } # float + locale fix $amount = number_format(floatval($amount), 2, '.', ''); return $paySystem->payForm( $way, $amount, $billID, _t('bills', 'Payment of bill #[bill.id] ([site.title])', [ 'bill.id' => $billID, 'site.title' => Site::title('bills.description.payform'), ]), $extra ); } /** * Формирование суммы для оплаты (оплачиваемая) * @param float $amount сумма оплаты (виртуальная) * @param string $key ключ в массиве способов оплаты $this->getPayWaysList * @return array ['amount'=>сумма для оплаты, 'currency'=>валюта для оплаты] */ public function getPayAmount($amount, $key) { $currencyDefault = Currency::id(); $result = [ 'amount' => $amount, 'currency' => $currencyDefault, ]; $payWays = $this->getPayWaysList(true); # не найден способ оплаты или сумма оплаты указана некорректно if (! isset($payWays[$key]) || $amount <= 0) { return $result; } $payWay = $payWays[$key]; if ( # внутренний счет (в основной валюте) $payWay['key'] === 'balance' || # валюта для данного способа оплаты не указана > считаем в основной валюте empty($payWay['currency_id']) || # валюта данного способа оплаты является основной валютой > сумма уже указана в основной валюте $payWay['currency_id'] == $currencyDefault ) { return $result; } # конвертируем в валюту оплаты $amountPay = Currency::convertPrice($amount, $currencyDefault, $payWay['currency_id']); if (fmod($amountPay, 1) > 0) { $amountPay = round($amountPay, 2); } return [ 'amount' => $amountPay, 'currency' => $payWay['currency_id'], ]; } /** * Формирование текста ошибки, возникающей в процессе обработки запроса от платежной системы * @param string $key ключ ошибки * @return \bff\http\Response */ public function payError($key) { $message = ''; switch ($key) { case 'off': $message = _t('bills', 'Payment method is disabled'); break; case 'no_params': $message = _t('bills', 'Parameters not passed'); break; case 'crc_error': $message = _t('bills', 'Invalid checksum'); break; case 'amount_error': $message = _t('bills', 'Invalid amount'); break; case 'wrong_bill_id': $message = _t('bills', 'Invalid account number format'); break; case 'pay_error': $message = _t('bills', 'Payment error'); break; case 'demo_fobidden': $message = _t('bills', 'Demo mode is disabled'); break; } return Response::text($message); } /** * Формируем список доступных статусов счета * @param array $skipStatus список статусов, которые необходимо исключить из результата * @return array */ public function getStatusData($skipStatus = []) { $list = $this->app->filter('bills.status.list', [ self::STATUS_COMPLETED => _t('bills', 'completed'), self::STATUS_WAITING => _t('bills', 'Not Finished'), self::STATUS_CANCELED => _t('bills', 'canceled'), self::STATUS_PROCESSING => _t('bills', 'processing'), ]); if (!empty($skipStatus)) { $list = array_diff_key($list, $skipStatus); } return $list; } /** * Формируем список статусов доступных для переключения из админ панели * @return array */ public function getStatusChangeAllowed() { $statuses = $this->getStatusData(); unset($statuses[static::STATUS_CANCELED]); unset($statuses[static::STATUS_COMPLETED]); return $this->app->filter('bills.status.change.allowed', array_keys($statuses)); } /** * Формируем список статусов счета в виде select:options * @param mixed $selected ID выбранного статуса * @param array $skipStatus список статусов, которые необходимо исключить из результата * @param mixed $emptyOption @see HTML::selectOptions * @return string */ protected function getStatusOptions($selected = null, $skipStatus = [], $emptyOption = false) { $list = $this->getStatusData($skipStatus); return HTML::selectOptions($list, $selected, $emptyOption); } /** * Формируем список типов счета * @param array $skipTypes список типов, которые необходимо исключить из результата * @return array */ public function getTypeOptions($skipTypes = []) { $list = [ self::TYPE_IN_PAY => _t('bills', 'Balance Top Up'), self::TYPE_OUT_ADMIN => _t('bills', 'Outgoing Payments'), self::TYPE_IN_GIFT => _t('bills', 'Gift'), self::TYPE_OUT_SERVICE => _t('bills', 'Payment for Services') ]; if (!empty($skipTypes)) { foreach ($skipTypes as $k) { if (isset($list[$k])) { unset($list[$k]); } } } return $list; } /** * Логирование процесса оплаты * @param string|array $message текст или данные для логирования * @param mixed $opts * @param mixed $to * @return void */ public function log($message, $opts = [], $to = null) { parent::log($message, $opts, $to ?? 'bills.log'); } /** * Названия активируемых услуг, после оплаты счета * @param int $billID * @return string */ public function billServicesTitles($billID) { do { $data = $this->model->billData($billID, ['service_settings']); if (empty($data['service_settings'])) { break; } $titles = Svc::servicesTitles($data['service_settings']); return join('; ', $titles); } while (false); return ''; } }