robokassa 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []