qiwi 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ require 'logger'
2
+
3
+ module Qiwi
4
+
5
+ class Config
6
+ attr_accessor :login, :password, :endpoint, :logger, :transaction_handler
7
+
8
+ def logger
9
+ @logger ||= Logger.new(STDERR)
10
+ end
11
+ end
12
+
13
+ def self.configure
14
+ yield config
15
+ end
16
+
17
+ def self.config
18
+ @config ||= Config.new
19
+ end
20
+
21
+ end
@@ -0,0 +1,8 @@
1
+ require 'rails'
2
+ require 'qiwi/server'
3
+
4
+ module Qiwi
5
+ class Engine < Rails::Engine
6
+ endpoint Qiwi::Server.new
7
+ end
8
+ end
@@ -0,0 +1,50 @@
1
+ require 'qiwi/transaction'
2
+
3
+ module Qiwi
4
+ class Handler
5
+ def self.call(txn, status)
6
+ new(txn, status).handle
7
+ end
8
+
9
+ attr_reader :txn, :status
10
+ def initialize(txn, status)
11
+ @txn = Transaction.new(txn)
12
+ @status = status
13
+ end
14
+
15
+ def handle
16
+ check_transaction
17
+ end
18
+
19
+ def check_transaction
20
+ unless txn.exists?
21
+ logger.error "Transaction doesn't exist: #{txn.txn}"
22
+ return 210
23
+ end
24
+
25
+ if status != txn.remote_status
26
+ logger.error "Stati don't match: #{txn.status} vs. #{status}"
27
+ return 300
28
+ end
29
+
30
+ unless txn.valid_amount?
31
+ logger.error "Incorrect amount: #{txn.amount}"
32
+ return 241
33
+ end
34
+
35
+ if txn.valid?
36
+ return 0
37
+ else
38
+ logger.error "Unknown error: #{txn.inspect}"
39
+ return 300
40
+ end
41
+
42
+ ensure
43
+ txn.commit!
44
+ end
45
+
46
+ def logger
47
+ Qiwi.logger
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,144 @@
1
+ require 'ostruct'
2
+ require 'active_model/validations'
3
+
4
+ module Qiwi
5
+ module Request
6
+
7
+ class Base
8
+ include ActiveModel::Validations
9
+
10
+ def self.inherited(klass)
11
+ klass.attributes :login, :password
12
+ klass.validates_presence_of :login, :password
13
+ end
14
+
15
+ def self.attributes(*attrs)
16
+ if attrs.empty?
17
+ @_attributes
18
+ else
19
+ @_attributes ||= []
20
+ @_attributes += attrs
21
+ attr_accessor(*attrs)
22
+ end
23
+ end
24
+
25
+ # See concrete classes for parameters description.
26
+ #
27
+ # @param [Qiwi::Client] client
28
+ # @param [Hash] params
29
+ def initialize(client, params)
30
+ self.class.attributes.each { |attr| send(:"#{attr}=", params[attr]) }
31
+ @login, @password = client.login, client.password
32
+ end
33
+
34
+ def body
35
+ with_envelope { |xml| soap_body(xml) }
36
+ end
37
+
38
+ def with_envelope(&block)
39
+ Nokogiri::XML::Builder.new do |xml|
40
+ xml.Envelope("xmlns:soapenv" => "http://www.w3.org/2003/05/soap-envelope",
41
+ "xmlns:tns" => "http://server.ishop.mw.ru/") do
42
+ xml.parent.namespace = xml.parent.namespace_definitions.first
43
+ xml.Header
44
+ xml.Body(&block)
45
+ end
46
+ end
47
+ end
48
+
49
+ def soap_body(xml)
50
+ xml['tns'].send(method) do
51
+ self.class.attributes.each do |attr|
52
+ # Underscore, so 'comment' is used as a parameter
53
+ # some ugly way to remove the namespace
54
+ xml.send("#{attr}_", send(attr)).instance_variable_get(:@node).namespace = nil
55
+ end
56
+ end
57
+ end
58
+
59
+ # The SOAP method name
60
+ def method
61
+ @method ||= self.class.to_s.split('::').last.camelize(:lower)
62
+ end
63
+
64
+ # Make sense of what is returned by the server
65
+ #
66
+ # @param [Nokogiri::XML::Document] xml
67
+ def result_from_xml(xml)
68
+ xml.xpath("//#{method}Response/#{method}Result").text.to_i
69
+ end
70
+ end
71
+
72
+ class CreateBill < Base
73
+ attributes :user, :amount, :comment, :txn, :lifetime, :alarm, :create
74
+
75
+ validates_presence_of :user, :amount, :txn
76
+ validates_numericality_of :amount
77
+ validates_length_of :comment, :maximum => 255
78
+ validates_length_of :txn, :maximum => 30
79
+ validates_format_of :lifetime, :with => /^\d{2}\.\d{2}\.\d{4}\s\d{2}:\d{2}:\d{2}$/, :allow_nil => true
80
+
81
+ # @param [Hash] params
82
+ # @option params [String] :user e.g. a phone number
83
+ # @option params [Float] :amount
84
+ # @option params [String] :comment
85
+ # @option params [String] :txn unique bill identifier
86
+ # @option params [String, Time] :lifetime in "dd.MM.yyyy HH:mm:ss" format
87
+ # @option params [Fixnum] :alarm
88
+ # @option params [Boolean] :create
89
+ def initialize(client, params)
90
+ super
91
+ @alarm ||= 0
92
+ @create = true if @create.nil?
93
+ @lifetime = @lifetime.strftime('%d.%m.%Y %H:%M:%S') if @lifetime.respond_to?(:strftime)
94
+ end
95
+ end
96
+
97
+ class CancelBill < Base
98
+ attributes :txn
99
+ end
100
+
101
+ class CheckBill < Base
102
+ attributes :txn
103
+
104
+ # @example
105
+ # {:user=>"name", :amount=>1000.0, :date=>"07.09.2012 13:33", :status=>60}
106
+ def result_from_xml(xml)
107
+ el = xml.xpath("//checkBillResponse")
108
+ OpenStruct.new({
109
+ user: el.at('user').text,
110
+ amount: el.at('amount').text.to_f,
111
+ date: el.at('date').text,
112
+ lifetime: el.at('lifetime').text,
113
+ status: el.at('status').text.to_i
114
+ })
115
+ end
116
+ end
117
+
118
+ class GetBillList < Base
119
+ attributes :dateFrom, :dateTo, :status
120
+
121
+ validates_presence_of :status
122
+ validates_numericality_of :status
123
+
124
+ # @param [Hash] params
125
+ # @option params [String] :user e.g. a phone number
126
+ # @option params [Time] :date_from
127
+ # @option params [Time] :date_to
128
+ # @option params [Fixnum] :status
129
+ def initialize(client, hash)
130
+ super
131
+ @dateFrom = hash[:date_from].strftime('%d.%m.%Y %H:%M:%S') if hash[:date_from]
132
+ @dateTo = hash[:date_to].strftime('%d.%m.%Y %H:%M:%S') if hash[:date_to]
133
+ end
134
+
135
+ def result_from_xml(xml)
136
+ el = xml.xpath("//getBillListResponse")
137
+ OpenStruct.new({
138
+ txns: el.at('txns').text,
139
+ count: el.at('count').text.to_i
140
+ })
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,24 @@
1
+ module Qiwi
2
+ module Response
3
+ STATUS = {
4
+ 50 => :set,
5
+ 52 => :being_processed,
6
+ 60 => :paid,
7
+ 150 => :cancelled_by_terminal,
8
+ 151 => :cancelled_no_auth,
9
+ 160 => :cancelled,
10
+ 161 => :cancelled_expired
11
+ }
12
+ STATUS.default_proc = lambda do |hash, status|
13
+ if status >= 51 and status <= 59
14
+ hash[52]
15
+ elsif status < 50
16
+ hash[50]
17
+ elsif status > 100
18
+ hash[160]
19
+ else
20
+ :unknown
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,78 @@
1
+ require 'nokogiri'
2
+ require 'digest/md5'
3
+ require 'qiwi/handler'
4
+
5
+ module Qiwi
6
+ class Server
7
+ attr_accessor :handler
8
+
9
+ # Creates a new instance of Rack application
10
+ #
11
+ # @param [String] login
12
+ # @param [String] password
13
+ # @param [Proc] handler must return 0 on success
14
+ def initialize(login = nil, password = nil, handler = nil)
15
+ @login = login || Qiwi.config.login
16
+ @password = password || Qiwi.config.password
17
+ @handler = handler || Qiwi::Handler
18
+ end
19
+
20
+ def call(env)
21
+ @logger = env['rack.logger']
22
+
23
+ if env['QUERY_STRING'] == 'wsdl'
24
+ body = File.read(File.join(File.dirname(__FILE__), 'IShopClientWS.wsdl'))
25
+ else
26
+ body = env['rack.input'].read
27
+ result = handle_soap_body(body)
28
+ body = <<-EOF
29
+ <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:cli="http://client.ishop.mw.ru/">
30
+ <soap:Body>
31
+ <cli:updateBillResponse>
32
+ <updateBillResult>#{result}</updateBillResult>
33
+ </cli:updateBillResponse>
34
+ </soap:Body>
35
+ </soap:Envelope>
36
+ EOF
37
+ end
38
+ headers = {'Content-Type' => 'application/soap+xml', 'Cache-Control' => 'no-cache'}
39
+ [200, headers, [body]]
40
+ end
41
+
42
+ private
43
+ def handle_soap_body(body)
44
+ xml = Nokogiri::XML(body).remove_namespaces!
45
+ nodeset = xml.xpath('//updateBill')
46
+ return 300 if nodeset.empty?
47
+
48
+ params = %w[login password txn status].each_with_object({}) do |field, h|
49
+ h[field.to_sym] = nodeset.at(field).text
50
+ end
51
+
52
+ unless authorized?(params)
53
+ logger.info "Unauthorized: #{params.inspect}"
54
+ return 150
55
+ end
56
+
57
+ txn, status = params.values_at(:txn, :status)
58
+ handler.call(txn, status.to_i).tap do |res|
59
+ logger.info "Qiwi handler returned #{res}"
60
+ end
61
+ rescue => e
62
+ # Unknown error
63
+ logger.error "Error: #{e.message}\n#{e.backtrace.slice(0,2).join("\n")}"
64
+ return 300
65
+ end
66
+
67
+ # Check if the password matches
68
+ def authorized?(params)
69
+ params[:login] == @login and
70
+ params[:password] == Digest::MD5.hexdigest(params[:txn] + Digest::MD5.hexdigest(@password).upcase).upcase
71
+ end
72
+
73
+ def logger
74
+ @logger ||= Logger.new(STDERR)
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,93 @@
1
+ require 'qiwi/client'
2
+ require 'observer'
3
+ require 'active_model'
4
+ require 'active_support/core_ext/module/delegation'
5
+
6
+ module Qiwi
7
+ class Transaction
8
+ include Observable
9
+ include ActiveModel::Validations
10
+
11
+ class CorrectAmountValidator < ActiveModel::Validator
12
+ def validate(txn)
13
+ unless txn.remote_amount.to_i == txn.amount.to_i
14
+ txn.errors.add(:amount, :invalid)
15
+ end
16
+ end
17
+ end
18
+
19
+ validates_presence_of :txn
20
+ validates_presence_of :persisted, :remote, :message => 'transaction not found'
21
+ validates :amount, :numericality => true, :correct_amount => true
22
+
23
+ delegate :amount, :status, :to => :remote, :prefix => true, :allow_nil => true
24
+
25
+ # Transaction id
26
+ attr_reader :txn
27
+
28
+ # Finder should respond_to?(:find_by_txn) and return an object,
29
+ # which can respond_to?(:amount)
30
+ attr_accessor :finder
31
+
32
+ def initialize(txn)
33
+ @txn = txn
34
+
35
+ # A logging observer
36
+ add_observer(self, :log_transaction)
37
+
38
+ if block_given?
39
+ yield self
40
+ else
41
+ Qiwi.config.transaction_handler.call(self) if Qiwi.config.transaction_handler
42
+ end
43
+ end
44
+
45
+ def inspect
46
+ error_msgs = errors.full_messages.join(', ')
47
+ %{<Qiwi::Transaction id: #{txn}, remote: #{remote.inspect} persisted: #{persisted.inspect} errors: #{error_msgs}}
48
+ end
49
+
50
+ def commit!
51
+ changed
52
+ notify_observers(self)
53
+ end
54
+
55
+ def exists?
56
+ !!persisted
57
+ end
58
+
59
+ def valid_amount?
60
+ valid?
61
+ errors[:amount].empty?
62
+ end
63
+
64
+ def amount
65
+ persisted.amount if exists?
66
+ end
67
+
68
+ def log_transaction(transaction)
69
+ Qiwi.logger.info "Transaction update: #{transaction.inspect}"
70
+ end
71
+
72
+ def remote
73
+ @remote ||= Qiwi::Client.new.check_bill(txn: txn)
74
+ end
75
+
76
+ def persisted
77
+ @persisted ||= find(txn)
78
+ end
79
+
80
+ def find(txn)
81
+ finder.find_by_txn(txn) if finder
82
+ end
83
+
84
+ end
85
+ end
86
+
87
+ # # Example:
88
+ # Qiwi.configure do |config|
89
+ # config.transaction_handler = lambda do |txn|
90
+ # txn.finder = PendingTransactions
91
+ # txn.add_observer(TransactionHandler.new)
92
+ # end
93
+ # end
@@ -0,0 +1,102 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "qiwi"
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Roman Shterenzon"]
12
+ s.date = "2012-10-31"
13
+ s.description = "Qiwi payments solution"
14
+ s.email = "romanbsd@yahoo.com"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".rspec",
21
+ "Gemfile",
22
+ "Gemfile.lock",
23
+ "Guardfile",
24
+ "LICENSE.txt",
25
+ "README.md",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "config/routes.rb",
29
+ "lib/qiwi.rb",
30
+ "lib/qiwi/IShopClientWS.wsdl",
31
+ "lib/qiwi/client.rb",
32
+ "lib/qiwi/config.rb",
33
+ "lib/qiwi/engine.rb",
34
+ "lib/qiwi/handler.rb",
35
+ "lib/qiwi/request.rb",
36
+ "lib/qiwi/response.rb",
37
+ "lib/qiwi/server.rb",
38
+ "lib/qiwi/transaction.rb",
39
+ "qiwi.gemspec",
40
+ "spec/client_spec.rb",
41
+ "spec/handler_spec.rb",
42
+ "spec/qiwi_spec.rb",
43
+ "spec/request_spec.rb",
44
+ "spec/server_spec.rb",
45
+ "spec/spec_helper.rb",
46
+ "spec/transaction_spec.rb"
47
+ ]
48
+ s.homepage = "http://github.com/romanbsd/qiwi"
49
+ s.licenses = ["MIT"]
50
+ s.require_paths = ["lib"]
51
+ s.rubygems_version = "1.8.24"
52
+ s.summary = "Qiwi payments solution"
53
+
54
+ if s.respond_to? :specification_version then
55
+ s.specification_version = 3
56
+
57
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
58
+ s.add_runtime_dependency(%q<activesupport>, [">= 0"])
59
+ s.add_runtime_dependency(%q<activemodel>, [">= 0"])
60
+ s.add_runtime_dependency(%q<nokogiri>, [">= 0"])
61
+ s.add_runtime_dependency(%q<faraday>, [">= 0"])
62
+ s.add_runtime_dependency(%q<rack>, [">= 0"])
63
+ s.add_development_dependency(%q<webmock>, [">= 0"])
64
+ s.add_development_dependency(%q<rspec>, ["~> 2.11.0"])
65
+ s.add_development_dependency(%q<yard>, ["~> 0.7"])
66
+ s.add_development_dependency(%q<bundler>, ["~> 1.0"])
67
+ s.add_development_dependency(%q<jeweler>, ["~> 1.8.4"])
68
+ s.add_development_dependency(%q<guard-rspec>, [">= 0"])
69
+ s.add_development_dependency(%q<rb-fsevent>, ["~> 0.9.1"])
70
+ s.add_development_dependency(%q<growl>, [">= 0"])
71
+ else
72
+ s.add_dependency(%q<activesupport>, [">= 0"])
73
+ s.add_dependency(%q<activemodel>, [">= 0"])
74
+ s.add_dependency(%q<nokogiri>, [">= 0"])
75
+ s.add_dependency(%q<faraday>, [">= 0"])
76
+ s.add_dependency(%q<rack>, [">= 0"])
77
+ s.add_dependency(%q<webmock>, [">= 0"])
78
+ s.add_dependency(%q<rspec>, ["~> 2.11.0"])
79
+ s.add_dependency(%q<yard>, ["~> 0.7"])
80
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
81
+ s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
82
+ s.add_dependency(%q<guard-rspec>, [">= 0"])
83
+ s.add_dependency(%q<rb-fsevent>, ["~> 0.9.1"])
84
+ s.add_dependency(%q<growl>, [">= 0"])
85
+ end
86
+ else
87
+ s.add_dependency(%q<activesupport>, [">= 0"])
88
+ s.add_dependency(%q<activemodel>, [">= 0"])
89
+ s.add_dependency(%q<nokogiri>, [">= 0"])
90
+ s.add_dependency(%q<faraday>, [">= 0"])
91
+ s.add_dependency(%q<rack>, [">= 0"])
92
+ s.add_dependency(%q<webmock>, [">= 0"])
93
+ s.add_dependency(%q<rspec>, ["~> 2.11.0"])
94
+ s.add_dependency(%q<yard>, ["~> 0.7"])
95
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
96
+ s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
97
+ s.add_dependency(%q<guard-rspec>, [">= 0"])
98
+ s.add_dependency(%q<rb-fsevent>, ["~> 0.9.1"])
99
+ s.add_dependency(%q<growl>, [">= 0"])
100
+ end
101
+ end
102
+