Skip to content

Instantly share code, notes, and snippets.

@c80609a
Last active October 5, 2019 03:34
Show Gist options
  • Save c80609a/af4e47e163995eebd3e05ba9e62b5b1b to your computer and use it in GitHub Desktop.
Save c80609a/af4e47e163995eebd3e05ba9e62b5b1b to your computer and use it in GitHub Desktop.
class YandexKassa
VAT_CODE_1 = 1 #Без НДС
VAT_CODE_2 = 2 #НДС по ставке 0%
VAT_CODE_3 = 3 #НДС по ставке 10%
VAT_CODE_4 = 4 #НДС чека по ставке 18%
VAT_CODE_5 = 5 #НДС чека по расчетной ставке 10/110
VAT_CODE_6 = 6 #НДС чека по расчетной ставке 18/118
class Error < StandardError
def initialize(message)
Rails.logger.error "\e[31m %s: %s \e[0m\n" % [self.class.name, message]
super message
end
end
class InvalidRequest < Error; end
class NotSupported < Error; end
class InvalidCredentials < Error; end
class Forbidden < Error; end
class NotFound < Error; end
class TooManyRequests < Error; end
class InternalServerError < Error; end
PAYMENT_PENDING = 'pending'.freeze
PAYMENT_SUCCEED = 'succeeded'.freeze
PAYMENT_CANCELED = 'canceled'.freeze
PAYMENT_WAITING_FOR_CAPTURE = 'waiting_for_capture'.freeze
def initialize(props)
@shop_key, @secret_key, @base_url =
props.symbolize_keys.values_at(:shop_key, :secret_key, :base_url)
@amount = nil
@currency = nil
@return_url = nil
@uuid = nil
@payload = nil
end
## Создание платежа на Яндекс.Кассе.
#
# @return [Hash]
#
# Пример ответа:
#
# {
# "id": "21740069-000f-50be-b000-0486ffbf45b0",
# "status": "pending",
# "paid": false,
# "amount": {
# "value": "2.00",
# "currency": "RUB"
# },
# "confirmation": {
# "type": "redirect",
# "confirmation_url": "https://money.yandex.ru/api-pages/v2/payment-confirm/epl?orderId=21740069-000f-50be-b000-0486ffbf45b0"
# },
# "created_at": "2017-10-14T10:53:29.072Z",
# "metadata": {},
# }
def payment_create(amount:, return_url:, currency:'RUB', uuid:, receipt: nil)
@amount = amount
@currency = currency
@return_url = return_url
@uuid = uuid
_make_create_payload(receipt)
_send_request
end
## Вернёт информацию о платеже.
#
# Пример ответа:
# {
# "id" => "21efe1d9-000f-5000-9000-1790d8f88b9b",
# "status" => "canceled",
# "paid" => false,
# "amount" => {
# "value"=>"50.00",
# "currency"=>"RUB"
# },
# "created_at" => "2018-01-16T10:04:36.712Z",
# "metadata" => {},
# "payment_method" => {
# "type" => "bank_card",
# "id" => "21efe1d9-000f-5000-9000-1790d8f88b9b",
# "saved" => false
# },
# "recipient" => {
# "account_id" => "501133",
# "gateway_id" => "1501133"
# }, "test" => true
# }
def payment_info(payment_id:, uuid:)
@uuid = uuid
url = '%s%s' % [@base_url, payment_id]
_make_info_payload
_send_request(url, :get)
end
def payment_capture(payment_id:, uuid:, amount:, currency:'RUB')
@uuid = uuid
@amount = amount
@currency = currency
url = '%s%s%s' % [@base_url, payment_id, '/capture']
_make_capture_payload
_send_request(url, :post)
end
private
def _make_create_payload(receipt)
@payload = {
amount: {
value: @amount,
currency: @currency
},
capture: false, # Платёж становится `waiting_for_capture` после `pending`, и мы должны принять его, чтобы завершить платёж
receipt: receipt,
confirmation: {
type: 'redirect', # способ оплаты юзер выбирает сам на веб-странице Яндекс.Кассы
return_url: @return_url # URL в кабинете, на который вернется пользователь после подтверждения или отмены платежа на веб-странице Яндекс.Кассы
}
}
end
def _make_info_payload
@payload = { }
end
def _make_capture_payload
@payload = {
amount: {
value: @amount,
currency: @currency
}
}
end
# @return [Hash]
def _send_request(url = @base_url, method = :post)
curl = Curl::Easy.new
curl.url = url
curl.timeout = 20
curl.connect_timeout = 20
curl.ssl_verify_peer = false
curl.verbose = true
curl.http_auth_types = [:basic]
curl.username = '%s:%s' % [@shop_key, @secret_key]
curl.headers['Idempotence-Key'] = @uuid
curl.headers['Content-Type'] = 'application/json'
_log_info_send(method, url, @payload, @shop_key, @secret_key)
# byebug
case method
when :get; curl.get
when :post; curl.post @payload.to_json
else raise StandardError.new 'Неизвестный http-метод'
end
response = JSON.load(curl.body_str) rescue { }
message = response['description'] rescue nil
message ||= '%s...' % curl.body_str[0..1024]
case curl.status.to_i
when 200
_log_info_answer response
response
when 202 # Запрос обрабатывается -- необходимо повторить запрос с тем же заголовком Idempotence-Key
# TODO:: необходимо повторить запрос с тем же заголовком Idempotence-Key [20171127]
when 400 # Неправильный запрос (response['code']=invalid_request,not_supported)
case response['code']
when 'invalid_request'
raise InvalidRequest.new message
when 'not_supported'
raise NotSupported.new message
else
raise Error.new message
end
when 401 # Неверный shop_id или secret_key (response['code']=invalid_credentials)
raise InvalidCredentials.new message
when 403 # Secret_key верный, но не хватает прав (response['code']=forbidden)
raise Forbidden.new message
when 404 # Ресурс не найден (response['code']=not_found)
raise NotFound.new message
when 429 # Превышен лимит запросов в ед. времени (response['code']=too_many_requests)
raise TooManyRequests.new message
when 500 # response['code']=internal_server_error
raise InternalServerError.new message
else
raise Error.new message
end
end # _send_request
def _log_info_send(method, url, data, shop_key, secret_key)
Rails.logger.info("\n\e[33m Started CURL %s %s \n Parameters: %s \n shop_key: %s \n secret_key: %s \n idempotence_key: %s \e[0m" % [method.to_s.upcase, url, data, shop_key, secret_key, @uuid])
end
def _log_info_answer(response)
Rails.logger.info("\e[32m Responce: %s \e[0m\n" % response)
end
end
module Advertisers
module Payments
# Запросить у Яндекс.Кассы информацию о платеже.
class GetInfoYandexKassaPayment
attr_reader :errors
attr_reader :payment_info
def initialize
@errors = Validators::Errors.new
@payment_info = nil
end
# @param [Billing::Request<ActiveRecord::Base>] request
def perform(request)
@errors = Validators::Errors.new
@payment_info = nil
yandex_client = YandexKassa.new YANDEX_CONFIG
begin
yandex_response = yandex_client.payment_info(
uuid: request.uuid,
payment_id: request.payment_id
)
rescue YandexKassa::InternalServerError => e
@errors.add('common', Validators::Rule::CONNECTION_ERROR)
return false
rescue YandexKassa::Error => e
@errors.add('common', Validators::Rule::SYSTEM_ERROR)
return false
end
# Ожидаем, что сервер пришлёт данные о платеже, т.е. в данных должны
# быть хотя бы +type+ и +object+.
# Если сервер прислал что-то другое -- считаем, что всё плохо.
if yandex_response
@payment_info = yandex_response
true
else
@errors.add('common', Validators::Rule::CONNECTION_ERROR)
false
end
end # def perform
end # class
end
end
class Advertiser::Payments::RequestsController < Advertiser::LayoutController
## GET yandex_kassa/return/:id
# После завершения оплаты на Яндекс.Кассе, юзера вернут по этому маршруту,
# и мы проверяем статус платежа, делая запрос на Яндекс.Кассу, только для того, чтобы
# показать соответствующее flash-сообщение.
# Параметр `:id` был вшит в `def yandex_kassa_create` -- чтобы распознать, статус какого платежа надо проверить.
def yandex_kassa_return
request = Billing::Request.find_by(id: params[:id], user_id: current_user.id)
if request
service = Advertisers::Payments::CheckYandexPaymentStatus.new # проверяем статус платежа, вычисляем, какое сообщение показать
if service.perform request
flash[:growl] = service.is_payment_succeed ? growl_success(service.growl) : growl_warning(service.growl) # Платёж (ПРОВЕДЁН || ОТМЕНЁН || ОЖИДАЕТ ПОДТВЕРЖДЕНИЯ || ЗАВЕРШЕН И ОЖИДАЕТ ВАШИХ ДЕЙСТВИЙ)
else
flash[:growl] = growl_error(service.errors.translate.values[0].first) # Validators::Rule::CONNECTION_ERROR || Validators::Rule::SYSTEM_ERROR
end
else
flash[:growl] = growl_error(I18n.t 'system.errors.record_not_found')
end
redirect_to payments_payers_path
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment