robokassa 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/.gitignore +8 -0
  2. data/Gemfile +24 -0
  3. data/README.md +189 -0
  4. data/Rakefile +26 -0
  5. data/VERSION +1 -0
  6. data/app/assets/images/robokassa/AlfaBank.gif +0 -0
  7. data/app/assets/images/robokassa/HandyBank.gif +0 -0
  8. data/app/assets/images/robokassa/Mastercard.gif +0 -0
  9. data/app/assets/images/robokassa/MoneyMail.gif +0 -0
  10. data/app/assets/images/robokassa/RBKMoney.gif +0 -0
  11. data/app/assets/images/robokassa/TeleMoney.gif +0 -0
  12. data/app/assets/images/robokassa/TerminalsAbsolutplat.gif +0 -0
  13. data/app/assets/images/robokassa/TerminalsElecsnet.gif +0 -0
  14. data/app/assets/images/robokassa/TerminalsPinpay.gif +0 -0
  15. data/app/assets/images/robokassa/TerminalsQiwi.gif +0 -0
  16. data/app/assets/images/robokassa/TerminalsUnikassa.gif +0 -0
  17. data/app/assets/images/robokassa/TerminalsmobilElement.gif +0 -0
  18. data/app/assets/images/robokassa/VKontakte.gif +0 -0
  19. data/app/assets/images/robokassa/VTB24.gif +0 -0
  20. data/app/assets/images/robokassa/Visa.gif +0 -0
  21. data/app/assets/images/robokassa/WMTrustID.gif +0 -0
  22. data/app/assets/images/robokassa/WebmoneyR.gif +0 -0
  23. data/app/assets/images/robokassa/YandexMoney.gif +0 -0
  24. data/app/assets/images/robokassa/iRobo.gif +0 -0
  25. data/app/assets/images/robokassa/logo.gif +0 -0
  26. data/app/controllers/robokassa_controller.rb +3 -0
  27. data/app/helpers/robokassa_helper.rb +130 -0
  28. data/app/views/payment_method/robokassa/_init.html.erb +32 -0
  29. data/autotest/discover.rb +1 -0
  30. data/config/locales/en.yml +3 -0
  31. data/config/locales/ru.yml +3 -0
  32. data/config/routes.rb +8 -0
  33. data/lib/robokassa.rb +20 -0
  34. data/lib/robokassa/controller.rb +19 -0
  35. data/lib/robokassa/interface.rb +378 -0
  36. data/robokassa.gemspec +35 -0
  37. data/spec/lib/interface_spec.rb +39 -0
  38. data/spec/routing/routes_spec.rb +23 -0
  39. data/spec/spec_helper.rb +14 -0
  40. metadata +199 -0
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ *.swp
6
+ *.swo
7
+ config.ru
8
+ spec/internal
data/Gemfile ADDED
@@ -0,0 +1,24 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in robokassa.gemspec
4
+ gemspec
5
+
6
+ gem 'nokogiri'
7
+
8
+ group :test do
9
+ gem 'ZenTest'
10
+ gem 'rspec'
11
+ gem 'rspec-rails', '>= 2.5.0'
12
+ gem 'factory_girl', '>= 1.3.3'
13
+ gem 'factory_girl_rails', '>= 1.0.1'
14
+ # gem 'rcov'
15
+ gem 'shoulda'
16
+ gem 'faker'
17
+ # gem 'celerity'
18
+ # gem 'culerity'
19
+ if RUBY_VERSION < "1.9"
20
+ gem "ruby-debug"
21
+ else
22
+ gem "ruby-debug19"
23
+ end
24
+ end
@@ -0,0 +1,189 @@
1
+ SUMMARY
2
+ -------
3
+
4
+ This gem adds robokassa support to your app.
5
+
6
+ Robokassa is payment system, that provides a single simple interface for payment systems popular in Russia.
7
+ If you have customers in Russia you can use the gem.
8
+
9
+ The first thing about this gem, is that it was oribinally desgned for spree commerce. So keep it im mind.
10
+
11
+ Данный джем является форком джема: https://github.com/shaggyone/robokassa
12
+
13
+ Using the Gem
14
+ -------------
15
+
16
+ Add the following line to your app Gemfile
17
+
18
+ gem 'robokassa'
19
+
20
+ Update your bundle
21
+
22
+ bundle install
23
+
24
+ ```ruby
25
+ config/initializers/robokassa.rb:
26
+
27
+ ROBOKASSA_SETTINGS = {
28
+ :test_mode => true,
29
+ :login => 'LOGIN',
30
+ :password1 => 'PASSWORD1',
31
+ :password2 => 'PASSWORD2'
32
+ }
33
+
34
+ $robokassa = Robokassa::Interface.new(ROBOKASSA_SETTINGS)
35
+
36
+ module Robokassa
37
+ class Interface
38
+ def notify_implementation(invoice_id, *args); end
39
+
40
+ class << self
41
+ def get_options_by_notification_key(key)
42
+ ROBOKASSA_SETTINGS
43
+ end
44
+
45
+ def success_implementation(invoice_id, *args)
46
+ payment = Payment.find_by_id(invoice_id)
47
+ payment.to_success!
48
+ end
49
+
50
+ def fail_implementation(invoice_id, *args)
51
+ payment = Payment.find_by_id(invoice_id)
52
+ payment.to_fail!
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ routes.rb:
59
+
60
+ ...
61
+ controller :robokassa do
62
+ get "robokassa/:notification_key/notify" => :notify, :as => :robokassa_notification
63
+ get "robokassa/success" => :success, :as => :robokassa_on_success
64
+ get "robokassa/fail" => :fail, :as => :robokassa_on_fail
65
+ end
66
+ ...
67
+
68
+ class Dashboard::PaymentsController < Dashboard::ApplicationController
69
+ ...
70
+ def create
71
+ @payment = current_user.payments.create!(:amount => params[:payment][:amount])
72
+ pay_url = $robokassa.init_payment_url(
73
+ @payment.id, @payment.amount, "Платеж № #{@payment.id}",
74
+ '', 'ru', current_user.email, {}
75
+ )
76
+ redirect_to pay_url
77
+ end
78
+ ...
79
+
80
+ class Payment < ActiveRecord::Base
81
+ include AASM
82
+
83
+ validates_presence_of :user_id, :amount
84
+ attr_accessible :amount
85
+ belongs_to :user
86
+
87
+ default_scope order("id desc")
88
+
89
+ aasm do
90
+ state :new, :initial => true
91
+ state :success
92
+ state :fail
93
+
94
+ event :to_success, :after => :give_money! do
95
+ transitions :to => :success
96
+ end
97
+
98
+ event :to_fail do
99
+ transitions :to => :fail
100
+ end
101
+ end
102
+
103
+ def state
104
+ self.aasm_state
105
+ end
106
+
107
+ def give_money!
108
+ self.user.give_money!(self.amount)
109
+ end
110
+
111
+ def printable_amount
112
+ "#{self.amount.to_s} руб."
113
+ end
114
+ end
115
+
116
+ class User < ActiveRecord::Base
117
+ ...
118
+ def give_money!(amount)
119
+ sql = "update users set balance='#{self.balance + amount.to_f}' where id='#{self.id}'"
120
+ connection.update(sql)
121
+ end
122
+ ...
123
+
124
+ dashboarb/payments/_form.html.erb:
125
+
126
+ <%= semantic_form_for [:dashboard, @payment] do |f| %>
127
+ <%= f.error_messages %>
128
+ <%= f.input :amount %>
129
+ <%= actions_for f, "Пополнить" %>
130
+ <% end %>
131
+
132
+ app/controllers/robokassa.rb:
133
+
134
+ # coding: utf-8
135
+ class RobokassaController < Robokassa::Controller
136
+ def success
137
+ super
138
+ @payment = Payment.find_by_id(params[:InvId])
139
+ if @payment
140
+ redirect_to dashboard_payment_path(@payment),
141
+ :notice => "Ваш платеж на сумму #{@payment.amount.to_s} руб. успешно принят. Спасибо!"
142
+ else
143
+ redirect_to new_dashboard_payment_path,
144
+ :error => "Не могу найти платеж по данному идентификатору"
145
+ end
146
+ end
147
+
148
+ def fail
149
+ super
150
+ redirect_to dashboard_payments_path,
151
+ :error => "Во время принятия платежа возникла ошибка. Мы скоро разберемся!"
152
+ end
153
+ end
154
+
155
+ ```
156
+
157
+ In Robokassa account settings set:
158
+
159
+ Result URL: http://example.com/robokassa/default/notify
160
+ Success URL: http://example.com/robokassa/success
161
+ Fail URL: http://example.com/robokassa/fail
162
+
163
+ Testing
164
+ -----
165
+ In console:
166
+
167
+ Clone gem
168
+ ```bash
169
+ git clone git://github.com/shaggyone/robokassa.git
170
+ ```
171
+
172
+ Install gems and generate a dummy application (It'll be ignored by git):
173
+ ```bash
174
+ cd robokassa
175
+ bundle install
176
+ bundle exec combust
177
+ ```
178
+
179
+ Run specs:
180
+ ```bash
181
+ rake spec
182
+ ```
183
+
184
+ Generate a dummy test application
185
+
186
+ Plans
187
+ -----
188
+
189
+ I plan to add generators for views
@@ -0,0 +1,26 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/packagetask'
5
+ require 'rubygems/package_task'
6
+ require 'bundler'
7
+ require 'rspec'
8
+ require "rspec/core/rake_task"
9
+
10
+ Bundler.setup
11
+ Bundler::GemHelper.install_tasks
12
+
13
+ RSpec::Core::RakeTask.new(:spec) do |spec|
14
+ spec.pattern = 'spec/**/*_spec.rb'
15
+ spec.rspec_opts = ['--backtrace']
16
+ end
17
+
18
+ require 'rdoc/task'
19
+ Rake::RDocTask.new do |rdoc|
20
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
21
+
22
+ rdoc.rdoc_dir = 'rdoc'
23
+ rdoc.title = "constantations #{version}"
24
+ rdoc.rdoc_files.include('README*')
25
+ rdoc.rdoc_files.include('lib/**/*.rb')
26
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.2
@@ -0,0 +1,3 @@
1
+ class RobokassaController < Robokassa::Controller
2
+
3
+ end
@@ -0,0 +1,130 @@
1
+ module RobokassaHelper
2
+ include ActionView::Helpers::TagHelper
3
+ include ActionView::Helpers::CaptureHelper
4
+ include ActionView::Helpers::FormTagHelper
5
+
6
+ PAYMENT_SYSTEMS = {
7
+ :bank_card => 'BANKOCEANMR', # Банковской картой
8
+ :platezh_ru => 'OceanBankOceanR', # Банковской картой через Platezh.ru
9
+ :qiwi => 'QiwiR', # QIWI Кошелек
10
+ :yandex => 'PCR', # Яндекс.Деньги
11
+ :wmr => 'WMRM', # WMR
12
+ :wmz => 'WMZM', # WMZ
13
+ :wme => 'WMEM', # WME
14
+ :wmu => 'WMUM', # WMU
15
+ :wmb => 'WMBM', # WMB
16
+ :wmg => 'WMGM', # WMG
17
+ :money_mail_ru => 'MoneyMailR', # RUR MoneyMail
18
+ :rur_rbk_money => 'RuPayR', # RUR RBK Money
19
+ :w1r => 'W1R', # RUR Единый Кошелек
20
+ :easy_pay => 'EasyPayB', # EasyPay
21
+ :liq_pay_usd => 'LiqPayZ', # USD LiqPay
22
+ :money_mail_ru => 'MailRuR', # Деньги@Mail.Ru
23
+ :z_payment => 'ZPaymentR', # RUR Z-Payment
24
+ :tele_money => 'TeleMoneyR', # RUR TeleMoney
25
+ :alfabank => 'AlfaBankR', # Альфа-Клик
26
+ :pskbr => 'PSKBR', # Промсвязьбанк
27
+ :handy_bank => 'HandyBankMerchantR', # HandyBank
28
+ :innivation => 'BSSFederalBankForInnovationAndDevelopmentR', # АК ФБ Инноваций и Развития (ЗАО)
29
+ :energobank => 'BSSMezhtopenergobankR', # Межтопэнергобанк
30
+ :svyaznoy => 'RapidaOceanSvyaznoyR', # Через Связной
31
+ :euroset => 'RapidaOceanEurosetR', # Через Евросеть
32
+ :elecsnet_r => 'ElecsnetR', # Элекснет
33
+ :kassira_net => 'TerminalsUnikassaR', # Кассира.нет
34
+ :mobil_element => 'TerminalsMElementR', # Мобил Элемент
35
+ :baltika => 'TerminalsNovoplatR', # Банк Балтика
36
+ :absolut_plat => 'TerminalsAbsolutplatR', # Absolutplat
37
+ :pinpay => 'TerminalsPinpayR', # Pinpay
38
+ :money_money => 'TerminalsMoneyMoneyR', # Money-Money
39
+ :petrocommerce => 'TerminalsPkbR', # Петрокоммерц
40
+ :vtb24 => 'VTB24R', # ВТБ24
41
+ :mts => 'MtsR', # МТС
42
+ :megafon => 'MegafonR', # Мегафон
43
+ :iphone => 'BANKOCEANCHECKR', # Через iPhone
44
+ :contact => 'ContactR', # Переводом по системе Контакт
45
+ :online_credit => 'OnlineCreditR'
46
+ }
47
+
48
+ def payment_form(interface, invoice_id, amount, description, custom_options = {})
49
+ payment_base_url = interface.init_payment_base_url
50
+ payment_options = interface.init_payment_options(invoice_id, amount, description, custom_options)
51
+
52
+ @__robokassa_vars = {
53
+ :interface => interface,
54
+ :invoice_id => invoice_id,
55
+ :amount => amount,
56
+ :description => description,
57
+ :custom_options => custom_options,
58
+ :payment_base_url => payment_base_url,
59
+ :payment_options => payment_options
60
+ }
61
+
62
+ if block_given?
63
+ yield payment_base_url, payment_options
64
+ else
65
+ render 'payment_method/robokassa/init',
66
+ :interface => interface,
67
+ :invoice_id => invoice_id,
68
+ :amount => amount,
69
+ :description => description,
70
+ :custom_options => custom_options,
71
+ :payment_base_url => payment_base_url,
72
+ :payment_options => payment_options
73
+ end
74
+
75
+ @__robokassa_vars = nil
76
+ end
77
+
78
+ def robokassa_rates_hash
79
+ raise "rates_hash helper should be called inside of payment_form." if @__robokassa_vars.blank?
80
+ @__robokassa_vars[:rates_hash] ||= robokassa_interface.rates_linear(robokassa_amount)
81
+ end
82
+
83
+ def robokassa_payment_block currency
84
+ raise "payment_block should be called inside of payment_form." if @__robokassa_vars.blank?
85
+
86
+ currency = RobokassaHelper::PAYMENT_SYSTEMS[currency]
87
+ raise "wrong currency" unless currency
88
+ kept_rate = @__robokassa_vars[:currency_rate]
89
+ currency_rate = robokassa_rates_hash[currency]
90
+ return "" unless currency_rate
91
+ @__robokassa_vars[:currency_rate] = currency_rate
92
+
93
+ yield currency_rate
94
+
95
+ @__robokassa_vars[:currency_rate] = kept_rate
96
+ end
97
+
98
+ def robokassa_payment_link *args
99
+ raise "payment_link should be called inside of payment_form." if @__robokassa_vars.blank?
100
+ options = args.extract_options!
101
+ currency = options.delete(:currency)
102
+ if currency
103
+ return robokassa_currency currency do
104
+ payment_link args
105
+ end
106
+ end
107
+ payment_url = "#{ robokassa_payment_base_url }?#{ robokassa_payment_options.merge('IncCurrLabel' => robokassa_currency_rate[:name]).to_query}"
108
+
109
+ if block_given?
110
+ link_to payment_url, *args, options do
111
+ yield robokassa_currency_rate
112
+ end
113
+ else
114
+ title = args.shift
115
+ link_to title, payment_url, *args, options
116
+ end
117
+ end
118
+
119
+ [:interface, :invoice_id, :amount, :description, :custom_options, :payment_base_url, :payment_options].each do |method_name|
120
+ define_method "robokassa_#{method_name}".to_sym do
121
+ raise "robokassa_#{method_name} helper should be called inside of payment_form." if @__robokassa_vars.blank?
122
+ @__robokassa_vars[method_name]
123
+ end
124
+ end
125
+
126
+ def robokassa_currency_rate
127
+ raise "robokassa_currency_rate helper should be called inside of robokassa_payment_block." if @__robokassa_vars.blank? || @__robokassa_vars[:currency_rate].blank?
128
+ @__robokassa_vars[:currency_rate]
129
+ end
130
+ end
@@ -0,0 +1,32 @@
1
+ <div class='robokassa'>
2
+ <form action="<%=interface.init_payment_base_url %>" method="get">
3
+ <%
4
+ options = interface.init_payment_options(invoice_id, amount, description, custom_options)
5
+ options.delete 'IncCurrLabel'
6
+ %>
7
+ <% options.each do |name, value| %>
8
+ <%= hidden_field_tag name, value %>
9
+ <% end %>
10
+ <ul class='robokassa-groups'>
11
+ <% interface.rates(amount).each_with_index do |group, i| %>
12
+ <% group_name, group = group %>
13
+ <li class='robokassa-group robokassa-<%= group_name.underscore %>'>
14
+ <p class='robokassa-group-description'><%= group[:description] %></p>
15
+ <ul>
16
+ <% group[:currencies].each_with_index do |currency, j| %>
17
+ <% currency_name, currency = currency %>
18
+ <li class='robokassa-group-currency <%= "#{group_name.underscore}_#{currency_name.underscore}" %>'>
19
+ <%= label_tag "inc_curr_label_#{group_name.underscore}_#{currency_name.underscore}" do %>
20
+ <%= radio_button_tag "IncCurrLabel", currency_name, false, :id => "inc_curr_label_#{group_name.underscore}_#{currency_name.underscore}" %>
21
+ <span class="description"><%= currency[:currency_description] %></span>
22
+ <span class="amount"><%= number_to_currency currency[:amount], :delimiter => " ", :format=>'%n' %></span>
23
+ <% end %>
24
+ </li>
25
+ <% end %>
26
+ </ul>
27
+ </li>
28
+ <% end %>
29
+ </ul>
30
+ <input type="submit" class="continue button primary" value="<%=t("robokassa.proceed_to_payment") %>" />
31
+ </form>
32
+ </div>
@@ -0,0 +1 @@
1
+ Autotest.add_discovery { "rspec2" }
@@ -0,0 +1,3 @@
1
+ en:
2
+ robokassa:
3
+ proceed_to_payment: "Proceed to payment"
@@ -0,0 +1,3 @@
1
+ ru:
2
+ robokassa:
3
+ proceed_to_payment: "Перейти к оплате"
@@ -0,0 +1,8 @@
1
+ Rails.application.routes.draw do
2
+ controller :robokassa do
3
+ get "robokassa/:notification_key/notify" => :notify, :as => :robokassa_notification
4
+
5
+ get "robokassa/success" => :success, :as => :robokassa_on_success
6
+ get "robokassa/fail" => :fail, :as => :robokassa_on_fail
7
+ end
8
+ end
@@ -0,0 +1,20 @@
1
+ module Robokassa
2
+ mattr_accessor :interface_class
3
+
4
+ # this allow use custom class for handeling api responces
5
+ # === Example
6
+ # Robokassa.interface_class = MyCustomInterface
7
+ # Robokassa.interface_class.new(options)
8
+ def self.interface_class
9
+ @@interface_class || ::Robokassa::Interface
10
+ end
11
+
12
+ class Engine < Rails::Engine #:nodoc:
13
+ config.autoload_paths += %W(#{config.root}/lib)
14
+
15
+ def self.activate
16
+ end
17
+
18
+ config.to_prepare &method(:activate).to_proc
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ class Robokassa::Controller < ActionController::Base
2
+ protect_from_forgery :only => []
3
+
4
+ def notify
5
+ interface = Robokassa.interface_class.create_by_notification_key params[:notification_key]
6
+ params.delete :notification_key
7
+ render :text => interface.notify(params, self)
8
+ end
9
+
10
+ def success
11
+ retval = Robokassa.interface_class.success(params, self)
12
+ redirect_to retval if retval.is_a? String
13
+ end
14
+
15
+ def fail
16
+ retval = Robokassa.interface_class.fail(params, self)
17
+ redirect_to retval if retval.is_a? String
18
+ end
19
+ end
@@ -0,0 +1,378 @@
1
+ require 'cgi'
2
+ require 'net/http'
3
+ require 'net/https'
4
+ require 'open-uri'
5
+ require 'rexml/document'
6
+
7
+ class Robokassa::Interface
8
+ include ActionDispatch::Routing::UrlFor
9
+ include Rails.application.routes.url_helpers
10
+
11
+ cattr_accessor :config
12
+
13
+ @@default_options = {
14
+ :language => "ru"
15
+ }
16
+ @cache = {}
17
+
18
+ # Indicate if calling api in test mode
19
+ # === Returns
20
+ # true or false
21
+ def test_mode?
22
+ @options[:test_mode] || false
23
+ end
24
+
25
+ def owner
26
+ @options[:owner]
27
+ end
28
+
29
+ # Takes options to access Robokassa API
30
+ #
31
+ # === Example
32
+ # Robokassa::Interface.new test_mode: true, login: 'demo', password1: '12345', password2: 'qweqwe123'
33
+ #
34
+ def initialize(options)
35
+ @options = @@default_options.merge(options.symbolize_keys)
36
+ @cache = {}
37
+ end
38
+
39
+ # This method verificates request params recived from robocassa server
40
+ def notify(params, controller)
41
+ parsed_params = map_params(params, @@notification_params_map)
42
+ notify_implementation(
43
+ parsed_params[:invoice_id],
44
+ parsed_params[:amount],
45
+ parsed_params[:custom_options],
46
+ controller)
47
+ "OK#{parsed_params[:invoice_id]}"
48
+ end
49
+
50
+ # Handler for success api callback
51
+ # this method calls from RobokassaController
52
+ # It requires Robokassa::Interface.success_implementation to be inmplemented by user
53
+ def self.success(params, controller)
54
+ parsed_params = map_params(params, @@notification_params_map)
55
+ success_implementation(
56
+ parsed_params[:invoice_id],
57
+ parsed_params[:amount],
58
+ parsed_params[:language],
59
+ parsed_params[:custom_options],
60
+ controller)
61
+ end
62
+
63
+ # Fail callback requiest handler
64
+ # It requires Robokassa::Interface.fail_implementation to be inmplemented by user
65
+ def self.fail(params, controller)
66
+ parsed_params = map_params(params, @@notification_params_map)
67
+ fail_implementation(
68
+ parsed_params[:invoice_id],
69
+ parsed_params[:amount],
70
+ parsed_params[:language],
71
+ parsed_params[:custom_options],
72
+ controller)
73
+ end
74
+
75
+
76
+ # Generates url for payment page
77
+ #
78
+ # === Example
79
+ # <%= link_to "Pay with Robokassa", interface.init_payment_url(order.id, order.amount, "Order #{order.id}", '', 'ru', order.user.email) %>
80
+ #
81
+ def init_payment_url(invoice_id, amount, description, currency='', language='ru', email='', custom_options={})
82
+ url_options = init_payment_options(invoice_id, amount, description, custom_options, currency, language, email)
83
+ "#{init_payment_base_url}?" + url_options.map do |k, v| "#{CGI::escape(k.to_s)}=#{CGI::escape(v.to_s)}" end.join('&')
84
+ end
85
+
86
+ def payment_methods # :nodoc:
87
+ return @cache[:payment_methods] if @cache[:payment_methods]
88
+ xml = get_remote_xml(payment_methods_url)
89
+ if xml.elements['PaymentMethodsList/Result/Code'].text != '0'
90
+ raise (a=xml.elements['PaymentMethodsList/Result/Description']) ? a.text : "Unknown error"
91
+ end
92
+
93
+ @cache[:payment_methods] ||= Hash[xml.elements.each('PaymentMethodsList/Methods/Method'){}.map do|g|
94
+ [g.attributes['Code'], g.attributes['Description']]
95
+ end]
96
+ end
97
+
98
+ def rates_long(amount, currency='')
99
+ cache_key = "rates_long_#{currency}_#{amount}"
100
+ return @cache[cache_key] if @cache[cache_key]
101
+ xml = get_remote_xml(rates_url(amount, currency))
102
+ if xml.elements['RatesList/Result/Code'].text != '0'
103
+ raise (a=xml.elements['RatesList/Result/Description']) ? a.text : "Unknown error"
104
+ end
105
+
106
+ @cache[cache_key] = Hash[xml.elements.each('RatesList/Groups/Group'){}.map do|g|
107
+ code = g.attributes['Code']
108
+ description = g.attributes['Description']
109
+ [
110
+ code,
111
+ {
112
+ :code => code,
113
+ :description => description,
114
+ :currencies => Hash[g.elements.each('Items/Currency'){}.map do|c|
115
+ label = c.attributes['Label']
116
+ name = c.attributes['Name']
117
+ [label, {
118
+ :currency => label,
119
+ :currency_description => name,
120
+ :group => code,
121
+ :group_description => description,
122
+ :amount => BigDecimal.new(c.elements['Rate'].attributes['IncSum'])
123
+ }]
124
+ end]
125
+ }
126
+ ]
127
+ end]
128
+ end
129
+
130
+ def rates(amount, currency='')
131
+ cache_key = "rates_#{currency}_#{amount}"
132
+ @cache[cache_key] ||= Hash[rates_long(amount, currency).map do |key, value|
133
+ [key, {
134
+ :description => value[:description],
135
+ :currencies => Hash[(value[:currencies] || []).map do |k, v|
136
+ [k, v]
137
+ end]
138
+ }]
139
+ end]
140
+ end
141
+
142
+ def rates_linear(amount, currency='')
143
+ cache_key = "rates_linear#{currency}_#{amount}"
144
+ @cache[cache_key] ||= begin
145
+ retval = rates(amount, currency).map do |group|
146
+ group_name, group = group
147
+ group[:currencies].map do |currency|
148
+ currency_name, currency = currency
149
+ {
150
+ :name => currency_name,
151
+ :desc => currency[:currency_description],
152
+ :group_name => group[:name],
153
+ :group_desc => group[:description],
154
+ :amount => currency[:amount]
155
+ }
156
+ end
157
+ end
158
+ Hash[retval.flatten.map { |v| [v[:name], v] }]
159
+ end
160
+ end
161
+
162
+ def currencies_long
163
+ return @cache[:currencies_long] if @cache[:currencies_long]
164
+ xml = get_remote_xml(currencies_url)
165
+ if xml.elements['CurrenciesList/Result/Code'].text != '0'
166
+ raise (a=xml.elements['CurrenciesList/Result/Description']) ? a.text : "Unknown error"
167
+ end
168
+ @cache[:currencies_long] = Hash[xml.elements.each('CurrenciesList/Groups/Group'){}.map do|g|
169
+ code = g.attributes['Code']
170
+ description = g.attributes['Description']
171
+ [
172
+ code,
173
+ {
174
+ :code => code,
175
+ :description => description,
176
+ :currencies => Hash[g.elements.each('Items/Currency'){}.map do|c|
177
+ label = c.attributes['Label']
178
+ name = c.attributes['Name']
179
+ [label, {
180
+ :currency => label,
181
+ :currency_description => name,
182
+ :group => code,
183
+ :group_description => description
184
+ }]
185
+ end]
186
+ }
187
+ ]
188
+ end]
189
+ end
190
+
191
+ def currencies
192
+ @cache[:currencies] ||= Hash[currencies_long.map do |key, value|
193
+ [key, {
194
+ :description => value[:description],
195
+ :currencies => value[:currencies]
196
+ }]
197
+ end]
198
+ end
199
+
200
+ # for testing
201
+ # === Example
202
+ # i.default_url_options = { :host => '127.0.0.1', :port => 3000 }
203
+ # i.notification_url # => 'http://127.0.0.1:3000/robokassa/asfadsf/notify'
204
+ def notification_url
205
+ robokassa_notification_url :notification_key => @options[:notification_key]
206
+ end
207
+
208
+ # for testing
209
+ def on_success_url
210
+ robokassa_on_success_url
211
+ end
212
+
213
+ # for testing
214
+ def on_fail_url
215
+ robokassa_on_fail_url
216
+ end
217
+
218
+ def parse_response_params(params)
219
+ parsed_params = map_params(params, @@notification_params_map)
220
+ parsed_params[:custom_options] = Hash[args.select do |k,v| o.starts_with?('shp') end.sort.map do|k, v| [k[3, k.size], v] end]
221
+ if response_signature(parsed_params)!=parsed_params[:signature].downcase
222
+ raise "Invalid signature"
223
+ end
224
+ end
225
+
226
+ def rates_url(amount, currency)
227
+ "#{xml_services_base_url}/GetRates?#{query_string(rates_options(amount, currency))}"
228
+ end
229
+
230
+ def rates_options(amount, currency)
231
+ map_params(subhash(@options.merge(:amount=>amount, :currency=>currency), %w{login language amount currency}), @@service_params_map)
232
+ end
233
+
234
+ def payment_methods_url
235
+ @cache[:get_currencies_url] ||= "#{xml_services_base_url}/GetPaymentMethods?#{query_string(payment_methods_options)}"
236
+ end
237
+
238
+ def payment_methods_options
239
+ map_params(subhash(@options, %w{login language}), @@service_params_map)
240
+ end
241
+
242
+ def currencies_url
243
+ @cache[:get_currencies_url] ||= "#{xml_services_base_url}/GetCurrencies?#{query_string(currencies_options)}"
244
+ end
245
+
246
+ def currencies_options
247
+ map_params(subhash(@options, %w{login language}), @@service_params_map)
248
+ end
249
+
250
+ # make hash of options for init_payment_url
251
+ def init_payment_options(invoice_id, amount, description, custom_options = {}, currency='', language='ru', email='')
252
+ options = {
253
+ :login => @options[:login],
254
+ :amount => amount.to_s,
255
+ :invoice_id => invoice_id,
256
+ :description => description[0, 100],
257
+ :signature => init_payment_signature(invoice_id, amount, description, custom_options),
258
+ :currency => currency,
259
+ :email => email,
260
+ :language => language
261
+ }.merge(Hash[custom_options.sort.map{|x| ["shp#{x[0]}", x[1]]}])
262
+ map_params(options, @@params_map)
263
+ end
264
+
265
+ # calculates signature to check params from Robokassa
266
+ def response_signature(parsed_params)
267
+ md5 response_signature_string(parsed_params)
268
+ end
269
+
270
+ # build signature string
271
+ def response_signature_string(parsed_params)
272
+ custom_options_fmt = custom_options.sort.map{|x|"shp#{x[0]}=x[1]]"}.join(":")
273
+ "#{parsed_params[:amount]}:#{parsed_params[:invoice_id]}:#{@options[:password2]}#{unless custom_options_fmt.blank? then ":" + custom_options_fmt else "" end}"
274
+ end
275
+
276
+ # calculates md5 from result of :init_payment_signature_string
277
+ def init_payment_signature(invoice_id, amount, description, custom_options={})
278
+ md5 init_payment_signature_string(invoice_id, amount, description, custom_options)
279
+ end
280
+
281
+ # generates signature string to calculate 'SignatureValue' url parameter
282
+ def init_payment_signature_string(invoice_id, amount, description, custom_options={})
283
+ custom_options_fmt = custom_options.sort.map{|x|"shp#{x[0]}=#{x[1]}"}.join(":")
284
+ "#{@options[:login]}:#{amount}:#{invoice_id}:#{@options[:password1]}#{unless custom_options_fmt.blank? then ":" + custom_options_fmt else "" end}"
285
+ end
286
+
287
+ # returns http://test.robokassa.ru or https://merchant.roboxchange.com in order to current mode
288
+ def base_url
289
+ test_mode? ? 'http://test.robokassa.ru' : 'https://merchant.roboxchange.com'
290
+ end
291
+
292
+ # returns url to redirect user to payment page
293
+ def init_payment_base_url
294
+ "#{base_url}/Index.aspx"
295
+ end
296
+
297
+ # returns base url for API access
298
+ def xml_services_base_url
299
+ "#{base_url}/WebService/Service.asmx"
300
+ end
301
+
302
+ @@notification_params_map = {
303
+ 'OutSum' => :amount,
304
+ 'InvId' => :invoice_id,
305
+ 'SignatureValue' => :signature,
306
+ 'Culture' => :language
307
+ }
308
+
309
+ @@params_map = {
310
+ 'MrchLogin' => :login,
311
+ 'OutSum' => :amount,
312
+ 'InvId' => :invoice_id,
313
+ 'Desc' => :description,
314
+ 'Email' => :email,
315
+ 'IncCurrLabel' => :currency,
316
+ 'Culture' => :language,
317
+ 'SignatureValue' => :signature
318
+ }.invert
319
+
320
+ @@service_params_map = {
321
+ 'MerchantLogin' => :login,
322
+ 'Language' => :language,
323
+ 'IncCurrLabel' => :currency,
324
+ 'OutSum' => :amount
325
+ }.invert
326
+
327
+ def md5(str) #:nodoc:
328
+ Digest::MD5.hexdigest(str).downcase
329
+ end
330
+
331
+ def subhash(hash, keys) #:nodoc:
332
+ Hash[keys.map do |key|
333
+ [key.to_sym, hash[key.to_sym]]
334
+ end]
335
+ end
336
+
337
+ # Maps gem parameter names, to robokassa names
338
+ def self.map_params(params, map)
339
+ Hash[params.map do|key, value| [(map[key] || map[key.to_sym] || key), value] end]
340
+ end
341
+
342
+ def map_params(params, map) #:nodoc:
343
+ self.class.map_params params, map
344
+ end
345
+
346
+ def query_string(params) #:nodoc:
347
+ params.map do |name, value|
348
+ "#{CGI::escape(name.to_s)}=#{CGI::escape(value.to_s)}"
349
+ end.join("&")
350
+ end
351
+
352
+ # make request and parse XML from specified url
353
+ def get_remote_xml(url)
354
+ # xml_data = Net::HTTP.get_response(URI.parse(url)).body
355
+ begin
356
+ xml_data = URI.parse(url).read
357
+ doc = REXML::Document.new(xml_data)
358
+ rescue REXML::ParseException => e
359
+ sleep 1
360
+ get_remote_xml(url)
361
+ end
362
+ end
363
+
364
+
365
+ class << self
366
+ # This method creates new instance of Interface for specified key (for multi-account support)
367
+ # it calls then Robokassa call ResultURL callback
368
+ def create_by_notification_key(key)
369
+ self.new get_options_by_notification_key(key)
370
+ end
371
+
372
+ %w{success fail notify}.map{|m| m + '_implementation'} + ['get_options_by_notification_key'].each do |m|
373
+ define_method m.to_sym do |*args|
374
+ raise NoMethodError, "Robokassa::Interface.#{m} should be defined by app developer"
375
+ end
376
+ end
377
+ end
378
+ end
@@ -0,0 +1,35 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ version = File.read(File.expand_path("../VERSION",__FILE__)).strip
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "robokassa"
7
+ s.version = version
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Victor Zagorski aka shaggyone"]
10
+ s.email = ["victor@zagorski.ru"]
11
+ s.homepage = "http://github.com/shaggyone/robokassa"
12
+ s.summary = %q{This gem adds robokassa support to your app.}
13
+ s.description = %q{
14
+ Robokassa is payment system, that provides a single simple interface for payment systems popular in Russia.
15
+ If you have customers in Russia you can use the gem.
16
+
17
+ The first thing about this gem, is that it was oribinally designed for spree commerce. So keep it in mind.
18
+ }
19
+
20
+ s.rubyforge_project = "robokassa"
21
+
22
+ s.files = `git ls-files`.split("\n")
23
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
24
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
25
+ s.require_paths = ["lib"]
26
+
27
+ s.add_dependency "rails", ">= 3.2.0"
28
+
29
+ s.add_development_dependency "rake"
30
+ s.add_development_dependency "thor"
31
+ s.add_development_dependency "shoulda"
32
+ s.add_development_dependency "bundler", ">= 1.0.0"
33
+ s.add_development_dependency 'combustion', '~> 0.3.1'
34
+ s.add_development_dependency 'sqlite3'
35
+ end
@@ -0,0 +1,39 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require 'robokassa/interface'
3
+ describe "Interface should work correct" do
4
+ before :each do
5
+ end
6
+
7
+ it "Should correctly use test server" do
8
+ i = Robokassa::Interface.new :test_mode => true
9
+ i.should be_test_mode
10
+ i.base_url.should == "http://test.robokassa.ru"
11
+ end
12
+
13
+ it "should compute correct signature string" do
14
+ i = Robokassa::Interface.new :test_mode => true, :login => 'demo', 'password1' => '12345'
15
+ i.init_payment_signature_string(15, 185.0, "Order #125").should == "demo:185.0:15:12345"
16
+ i.init_payment_signature_string(15, 185.0, "Order #125", {:a => 15, :c => 30, :b => 20}).should == "demo:185.0:15:12345:shpa=15:shpb=20:shpc=30"
17
+ end
18
+
19
+ it "should create correct init payment url" do
20
+ i = Robokassa::Interface.new :test_mode => true, :login => 'demo', 'password1' => '12345'
21
+ i = Robokassa::Interface.new :test_mode => true, :login => 'shaggyone239', 'password1' => '12345asdf'
22
+ i.init_payment_signature_string(15, 185.11, "Order #125").should == "shaggyone239:185.11:15:12345asdf"
23
+ i.init_payment_signature(15, 185.11, "Order #125").should == "55f2aee20767cde28e7fc49919cec969"
24
+ i.init_payment_url(15, 185.11, "Order 125", '', 'ru', 'demo@robokassa.ru', {}).should ==
25
+ "http://test.robokassa.ru/Index.aspx?MrchLogin=shaggyone239&OutSum=185.11&InvId=15&Desc=Order+125&SignatureValue=55f2aee20767cde28e7fc49919cec969&IncCurrLabel=&Email=demo%40robokassa.ru&Culture=ru"
26
+ i.init_payment_signature(196, 2180.0, "R602412577").should == "adeedf2afbac5eca09b44898da3ef51a"
27
+ end
28
+
29
+ it "should return correct notification, success and fail urls" do
30
+ i = Robokassa::Interface.new :test_mode => true, :login => 'demo', 'password1' => '12345', :notification_key => "asfadsf"
31
+ i.default_url_options = {
32
+ :host => '127.0.0.1',
33
+ :port => 3000
34
+ }
35
+ i.notification_url.should == 'http://127.0.0.1:3000/robokassa/asfadsf/notify'
36
+ i.on_success_url.should == 'http://127.0.0.1:3000/robokassa/success'
37
+ i.on_fail_url.should == 'http://127.0.0.1:3000/robokassa/fail'
38
+ end
39
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ describe 'routes' do
5
+ context "robokassa" do
6
+ specify { get('/robokassa/some-secure-notification-key/notify').should route_to(
7
+ controller: 'robokassa',
8
+ action: 'notify',
9
+ notification_key: 'some-secure-notification-key'
10
+ )}
11
+
12
+ specify { get('/robokassa/success').should route_to(
13
+ controller: 'robokassa',
14
+ action: 'success'
15
+ )}
16
+
17
+ specify { get('/robokassa/fail').should route_to(
18
+ controller: 'robokassa',
19
+ action: 'fail'
20
+ )}
21
+ end
22
+ end
23
+
@@ -0,0 +1,14 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ require 'rails'
5
+
6
+ Bundler.require :default, :development
7
+
8
+ Combustion.initialize! :active_record, :action_controller
9
+
10
+ require 'rspec/rails'
11
+
12
+ RSpec.configure do |config|
13
+ config.use_transactional_fixtures = true
14
+ end
metadata ADDED
@@ -0,0 +1,199 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: robokassa
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Victor Zagorski aka shaggyone
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-31 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.2.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 3.2.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: thor
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: shoulda
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: bundler
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: 1.0.0
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: 1.0.0
94
+ - !ruby/object:Gem::Dependency
95
+ name: combustion
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: 0.3.1
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: 0.3.1
110
+ - !ruby/object:Gem::Dependency
111
+ name: sqlite3
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: ! "\n Robokassa is payment system, that provides a single simple interface
127
+ for payment systems popular in Russia.\n If you have customers in Russia you
128
+ can use the gem.\n\n The first thing about this gem, is that it was oribinally
129
+ designed for spree commerce. So keep it in mind.\n "
130
+ email:
131
+ - victor@zagorski.ru
132
+ executables: []
133
+ extensions: []
134
+ extra_rdoc_files: []
135
+ files:
136
+ - .gitignore
137
+ - Gemfile
138
+ - README.md
139
+ - Rakefile
140
+ - VERSION
141
+ - app/assets/images/robokassa/AlfaBank.gif
142
+ - app/assets/images/robokassa/HandyBank.gif
143
+ - app/assets/images/robokassa/Mastercard.gif
144
+ - app/assets/images/robokassa/MoneyMail.gif
145
+ - app/assets/images/robokassa/RBKMoney.gif
146
+ - app/assets/images/robokassa/TeleMoney.gif
147
+ - app/assets/images/robokassa/TerminalsAbsolutplat.gif
148
+ - app/assets/images/robokassa/TerminalsElecsnet.gif
149
+ - app/assets/images/robokassa/TerminalsPinpay.gif
150
+ - app/assets/images/robokassa/TerminalsQiwi.gif
151
+ - app/assets/images/robokassa/TerminalsUnikassa.gif
152
+ - app/assets/images/robokassa/TerminalsmobilElement.gif
153
+ - app/assets/images/robokassa/VKontakte.gif
154
+ - app/assets/images/robokassa/VTB24.gif
155
+ - app/assets/images/robokassa/Visa.gif
156
+ - app/assets/images/robokassa/WMTrustID.gif
157
+ - app/assets/images/robokassa/WebmoneyR.gif
158
+ - app/assets/images/robokassa/YandexMoney.gif
159
+ - app/assets/images/robokassa/iRobo.gif
160
+ - app/assets/images/robokassa/logo.gif
161
+ - app/controllers/robokassa_controller.rb
162
+ - app/helpers/robokassa_helper.rb
163
+ - app/views/payment_method/robokassa/_init.html.erb
164
+ - autotest/discover.rb
165
+ - config/locales/en.yml
166
+ - config/locales/ru.yml
167
+ - config/routes.rb
168
+ - lib/robokassa.rb
169
+ - lib/robokassa/controller.rb
170
+ - lib/robokassa/interface.rb
171
+ - robokassa.gemspec
172
+ - spec/lib/interface_spec.rb
173
+ - spec/routing/routes_spec.rb
174
+ - spec/spec_helper.rb
175
+ homepage: http://github.com/shaggyone/robokassa
176
+ licenses: []
177
+ post_install_message:
178
+ rdoc_options: []
179
+ require_paths:
180
+ - lib
181
+ required_ruby_version: !ruby/object:Gem::Requirement
182
+ none: false
183
+ requirements:
184
+ - - ! '>='
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ required_rubygems_version: !ruby/object:Gem::Requirement
188
+ none: false
189
+ requirements:
190
+ - - ! '>='
191
+ - !ruby/object:Gem::Version
192
+ version: '0'
193
+ requirements: []
194
+ rubyforge_project: robokassa
195
+ rubygems_version: 1.8.19
196
+ signing_key:
197
+ specification_version: 3
198
+ summary: This gem adds robokassa support to your app.
199
+ test_files: []