supreme 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Fingertips, Manfred Stienstra <manfred@fngtps.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,67 @@
1
+ A Ruby client that allows you to do iDEAL transactions through the [Mollie iDEAL API](http://www.mollie.nl/betaaldiensten/ideal).
2
+
3
+ ## Install
4
+
5
+ $ gem install supreme
6
+
7
+ ## Payment flow
8
+
9
+ You start by setting the mode of the library to either :test or :live. The default is :test, so
10
+ for tests you don't have to change anything.
11
+
12
+ Supreme.mode = :live
13
+
14
+ You can choose to set the partner (client) id globally so you don't have to include it with every
15
+ call.
16
+
17
+ Supreme.partner_id = '000000'
18
+
19
+ Then you get a list of supported banks. Note that this list can change at any time, so be conservative
20
+ with caching these values.
21
+
22
+ Supreme.api.bank_list #=> [["ABN AMRO", "0031"], ["Postbank", "0721"], ["Rabobank", "0021"]]
23
+
24
+ When the user has selected a bank, you start a transaction.
25
+
26
+ transaction = Supreme.api.fetch(
27
+ :bank_id => '0031',
28
+ :amount => 1299,
29
+ :description => 'A fluffy bunny',
30
+ :return_url => 'http://example.com/payments/as45re/thanks',
31
+ :report_url => 'http://example.com/payments/as45re'
32
+ )
33
+ transaction.transaction_id #=> '482d599bbcc7795727650330ad65fe9b'
34
+
35
+ Keep the transaction_id around for reference and redirect the customer to the indicated URL.
36
+
37
+ Location: #{transaction.url}
38
+
39
+ Once the transaction is done you will receive a GET on the report_url with a ‘transaction_id’ parameter
40
+ to indicate that the transaction has changed state. You will need to check the status of the transaction.
41
+
42
+ status = Supreme.api.check(
43
+ :transaction_id => '482d599bbcc7795727650330ad65fe9b'
44
+ )
45
+
46
+ # Note that the status will only be paid? after the first check, for each consecutive call
47
+ # paid? will be false regardless of the outcome of the transaction.
48
+ #
49
+ # We use success? and make sure we don't deliver the product more than once.
50
+ if status.success?
51
+ # Update the local status of the payment
52
+ end
53
+
54
+ When the customer returns to your site it returns with its transaction_id attached to your provided URL.
55
+ You can present a page depending on the status of the payment.
56
+
57
+ ## Errors
58
+
59
+ When an error occurs you get a Supreme::Error object instead of the response object you expected.
60
+
61
+ status = Supreme.api.check(
62
+ :transaction_id => '482d599bbcc7795727650330ad65fe9b'
63
+ )
64
+
65
+ if status.error?
66
+ log("#{status.message} (#{status.code})")
67
+ end
@@ -0,0 +1,47 @@
1
+ # Supreme is a Ruby for doing iDEAL transactions through the Mollie iDEAL API.
2
+ # http://www.mollie.nl/betaaldiensten/ideal
3
+ #
4
+ # To get going read the documentation for Supreme and Supreme::API.
5
+
6
+ require 'supreme/uri'
7
+ require 'supreme/api'
8
+ require 'supreme/version'
9
+ require 'supreme/response'
10
+
11
+ module Supreme
12
+ class << self
13
+ # Holds either :test or :live to signal whether to run in test or live mode. The default
14
+ # value is :test.
15
+ attr_accessor :mode
16
+
17
+ # Your Mollie Partner ID, you can find it under ‘Accountgegevens’ in the settings for your account on mollie.nl.
18
+ attr_accessor :partner_id
19
+ end
20
+
21
+ # Returns an instance of the API with settings from the Supreme class accessors. If you need to handle
22
+ # multiple accounts in your application you will need to instantiate multiple API instances yourself.
23
+ def self.api
24
+ Supreme::API.new(
25
+ :mode => mode,
26
+ :partner_id => partner_id
27
+ )
28
+ end
29
+
30
+ # Resets the class back to the default settings
31
+ def self.reset!
32
+ self.mode = :test
33
+ self.partner_id = nil
34
+ end
35
+
36
+ reset!
37
+
38
+ private
39
+
40
+ def self.translate_hash_keys(translation, hash)
41
+ translated = {}
42
+ hash.each do |key, value|
43
+ new_key = translation[key.to_sym] || translation[key.to_s] ||key
44
+ translated[new_key.to_s] = value
45
+ end; translated
46
+ end
47
+ end
@@ -0,0 +1,148 @@
1
+ require 'uri'
2
+ require 'rest'
3
+
4
+ module Supreme
5
+ class API
6
+ ENDPOINT = ::URI.parse("https://secure.mollie.nl/xml/ideal")
7
+
8
+ attr_accessor :mode, :partner_id
9
+
10
+ # Creates a new API instance. Normally you would use <tt>Supreme.api</tt> to create an
11
+ # API instance. If you have to interact with multiple accounts in one process you can
12
+ # instantiate the API class youself.
13
+ #
14
+ # === Options
15
+ #
16
+ # * <tt>:mode</tt> – Holds either :test or :live to signal whether to run in test or live mode. The default value is :test.
17
+ # * <tt>:partner_id</tt> – Your Mollie Partner ID, you can find it under ‘Accountgegevens’ in the settings for your account on mollie.nl.
18
+ #
19
+ # === Example
20
+ #
21
+ # api = Supreme::API.new(:partner_id => '9834234')
22
+ # api.test? #=> true
23
+ # api.banklist
24
+ def initialize(options={})
25
+ self.mode = options[:mode] || options['mode'] || :test
26
+ self.partner_id = options[:partner_id] || options['partner_id']
27
+ end
28
+
29
+ # Returns true when we're in test mode and false otherwise
30
+ def test?
31
+ self.mode.to_sym == :test
32
+ end
33
+
34
+ # Requests a list of available banks. Turns a Banklist response.
35
+ # Use Banklist#to_a to get a list of hashes with actual information.
36
+ #
37
+ # Supreme.api.banklist.to_a # => [{ :id => '1006', :name => 'ABN AMRO Bank' }, …]
38
+ #
39
+ # Returns a Supreme::Error when the call fails.
40
+ def banklist
41
+ response = get('banklist')
42
+ log('Banklist response', response.body)
43
+ Supreme::Response.for(response.body, Supreme::Banklist)
44
+ end
45
+
46
+ # Starts a new payment by sending payment information. It also configures how the iDEAL
47
+ # provider handles payment status information. It returns a Supreme::Transaction response
48
+ # object.
49
+ #
50
+ # Returns a Supreme::Error when the call fails.
51
+ #
52
+ # === Options
53
+ #
54
+ # Note that the <tt>:description</tt> option has a character limit of 29 characters.
55
+ # Anything after the 29 characters will be silently removed by the API. Note that
56
+ # this description might be handled by ancient bank systems and anything but ASCII
57
+ # characters might be mangled or worse.
58
+ #
59
+ # ==== Required
60
+ #
61
+ # * <tt>:bank_id</tt> – The bank selected by the customer from the <tt>banklist</tt>.
62
+ # * <tt>:amount</tt> – The amount you want to charge in cents (EURO) (ie. €12,99 is 1299)
63
+ # * <tt>:description</tt> – Describe what the payment is for (max 29 characters) (ie. ‘Fluffy Bunny (sku 1234)’ )
64
+ # * <tt>:report_url</tt> – You will receive a GET to this URL with the transaction_id appended in the query (ie. http://example.com/payments?transaction_id=23ad33)
65
+ # * <tt>:return_url</tt> – The customer is redirected to this URL after the payment is complete. The transaction_id is appended as explained for <tt>:report_url</tt>
66
+ #
67
+ # ==== Optional
68
+ #
69
+ # * <tt>:partner_id</tt> – Your Mollie Partner ID, you can find it under ‘Accountgegevens’ in the settings for your account on mollie.nl. Note that the Partner ID is only optional if you've set it either on the API instance or using <tt>Supreme.partner_id</tt>.
70
+ # * <tt>:profile_key</tt> – When your account receives payment from different websites or companies you can set up company profiles. See the Mollie documentation for more information: http://www.mollie.nl/support/documentatie/betaaldiensten/ideal/.
71
+ #
72
+ # === Example
73
+ #
74
+ # transaction = Supreme.api.fetch({
75
+ # :bank_id => '0031',
76
+ # :amount => 1299,
77
+ # :description => '20 credits for your account',
78
+ # :report_url => 'http://example.com/payments/ad74hj23',
79
+ # :return_url => 'http://example.com/payments/ad74hj23/thanks'
80
+ # })
81
+ # @purchase.update_attributes!(:transaction_id => transaction.transaction_id)
82
+ #
83
+ # See the Supreme::Transaction class for more information.
84
+ def fetch(options)
85
+ options = options.dup
86
+ options[:partner_id] ||= partner_id
87
+ response = get('fetch', Supreme.translate_hash_keys({
88
+ :partner_id => :partnerid,
89
+ :return_url => :returnurl,
90
+ :report_url => :reporturl
91
+ }, options))
92
+ log('Fetch response', response.body)
93
+ Supreme::Response.for(response.body, Supreme::Transaction)
94
+ end
95
+
96
+ # Requests the status information for a payment. It returns a Supreme::Status
97
+ # response object.
98
+ #
99
+ # Returns a Supreme::Error when the call fails.
100
+ #
101
+ # === Options
102
+ #
103
+ # * <tt>:transaction_id</tt> – The transaction ID you received earlier when setting up the transaction.
104
+ #
105
+ # == Example
106
+ #
107
+ # status = Supreme.api.check(:transaction_id => '482d599bbcc7795727650330ad65fe9b')
108
+ # if status.paid?
109
+ # @purchase.paid!
110
+ # end
111
+ #
112
+ # See the Supreme::Status class for more information.
113
+ def check(options)
114
+ options = options.dup
115
+ options[:partner_id] ||= partner_id
116
+ response = get('check', Supreme.translate_hash_keys({
117
+ :partner_id => :partnerid,
118
+ }, options))
119
+ log('Status response', response.body)
120
+ Supreme::Response.for(response.body, Supreme::Status)
121
+ end
122
+
123
+ private
124
+
125
+ def endpoint
126
+ ENDPOINT.dup
127
+ end
128
+
129
+ def query(options={})
130
+ options = options.dup
131
+ options[:testmode] = 'true' if test?
132
+ options == {} ? nil : Supreme::URI.generate_query(options)
133
+ end
134
+
135
+ def get(action, options={})
136
+ options = options.dup
137
+ options[:a] = action
138
+ url = endpoint
139
+ url.query = query(options)
140
+ log('GET', url.to_s)
141
+ REST.get(url.to_s)
142
+ end
143
+
144
+ def log(thing, contents)
145
+ $stderr.write("\n#{thing}:\n\n#{contents}\n") if $DEBUG
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,133 @@
1
+ require 'rexml/document'
2
+
3
+ module Supreme
4
+ # The base class for all response classes.
5
+ class Response
6
+ attr_accessor :body
7
+
8
+ def initialize(body)
9
+ @body = body
10
+ end
11
+
12
+ def error?
13
+ false
14
+ end
15
+
16
+ # Return an instance of a reponse class based on the contents of the body
17
+ def self.for(response_body, klass)
18
+ body = REXML::Document.new(response_body).root
19
+ if body.elements["/response/item"]
20
+ ::Supreme::Error.new(body)
21
+ else
22
+ klass.new(body)
23
+ end
24
+ end
25
+
26
+ protected
27
+
28
+ def text(path)
29
+ @body.get_text(path).to_s
30
+ end
31
+ end
32
+
33
+ # Response to a banklist request
34
+ class Banklist < Response
35
+ def to_a
36
+ @body.get_elements('//bank').map do |issuer|
37
+ { :id => issuer.get_text('bank_id').to_s, :name => issuer.get_text('bank_name').to_s }
38
+ end
39
+ end
40
+ end
41
+
42
+ # Response to a fetch request
43
+ class Transaction < Response
44
+ def transaction_id
45
+ text('//transaction_id')
46
+ end
47
+
48
+ def amount
49
+ text('//amount')
50
+ end
51
+
52
+ def url
53
+ text('//URL')
54
+ end
55
+ end
56
+
57
+ # Response to a check request
58
+ class Status < Response
59
+ def transaction_id
60
+ text('//transaction_id')
61
+ end
62
+
63
+ def amount
64
+ text('//amount')
65
+ end
66
+
67
+ def currency
68
+ text('//currency')
69
+ end
70
+
71
+ def paid
72
+ text('//payed')
73
+ end
74
+
75
+ # A payment will return paid? for just one request. If you issue it too early you might never
76
+ # get a truthy value from this.
77
+ def paid?
78
+ paid == 'true'
79
+ end
80
+
81
+ # Returns the status of the payment. This is probably the best way to check if the payment has
82
+ # succeeded. It returns one of the following values:
83
+ #
84
+ # * <tt>Open</tt> – Payment is still processing.
85
+ # * <tt>Success</tt> – The payment was successful.
86
+ # * <tt>Cancelled</tt> – The payment was explicitly cancelled by the customer.
87
+ # * <tt>Failure</tt> – The payment failed.
88
+ # * <tt>Expired</tt> – The customer abandoned the payment, we don't expect them to finish it.
89
+ # * <tt>CheckedBefore</tt> – You've requested the payment status before.
90
+ #
91
+ # You can also check the status of the payment with one of the boolean accessors: open?, success?,
92
+ # cancelled?, failed?, expired?, and checked_before?.
93
+ def status
94
+ text('//status')
95
+ end
96
+
97
+ [
98
+ ['Open', :open?],
99
+ ['Success', :success?],
100
+ ['Cancelled', :cancelled?],
101
+ ['Failure', :failed?],
102
+ ['Expired', :expired?],
103
+ ['CheckedBefore', :checked_before?]
104
+ ].each do |expected, accessor|
105
+ define_method accessor do
106
+ status == expected
107
+ end
108
+ end
109
+
110
+ def customer
111
+ {
112
+ 'name' => text('//consumerName'),
113
+ 'account' => text('//consumerAccount'),
114
+ 'city' => text('//consumerCity')
115
+ }
116
+ end
117
+ end
118
+
119
+ # Response was an error
120
+ class Error < Response
121
+ def error?
122
+ true
123
+ end
124
+
125
+ def code
126
+ text('//errorcode')
127
+ end
128
+
129
+ def message
130
+ text('//message')
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,88 @@
1
+ module Supreme
2
+ class URI
3
+ # Borrowed from uri/common.rb, with modifications to support 1.8.x
4
+ # https://github.com/ruby/ruby/blob/trunk/lib/uri/common.rb
5
+
6
+ TBLENCWWWCOMP_ = {} # :nodoc:
7
+ TBLDECWWWCOMP_ = {} # :nodoc:
8
+
9
+ # Encode given +s+ to URL-encoded form data.
10
+ #
11
+ # This method doesn't convert *, -, ., 0-9, A-Z, _, a-z, but does convert SP
12
+ # (ASCII space) to + and converts others to %XX.
13
+ #
14
+ # This is an implementation of
15
+ # http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
16
+ #
17
+ # See URI.decode_www_form_component, URI.encode_www_form
18
+ def self.encode_www_form_component(s)
19
+ str = s.to_s
20
+ if RUBY_VERSION < "1.9" && $KCODE =~ /u/i
21
+ str.gsub(/([^ a-zA-Z0-9_.-]+)/) do
22
+ '%' + $1.unpack('H2' * Rack::Utils.bytesize($1)).join('%').upcase
23
+ end.tr(' ', '+')
24
+ else
25
+ if TBLENCWWWCOMP_.empty?
26
+ tbl = {}
27
+ 256.times do |i|
28
+ tbl[i.chr] = '%%%02X' % i
29
+ end
30
+ tbl[' '] = '+'
31
+ begin
32
+ TBLENCWWWCOMP_.replace(tbl)
33
+ TBLENCWWWCOMP_.freeze
34
+ rescue
35
+ end
36
+ end
37
+ str.gsub(/[^*\-.0-9A-Z_a-z]/) {|m| TBLENCWWWCOMP_[m]}
38
+ end
39
+ end
40
+
41
+ # Decode given +str+ of URL-encoded form data.
42
+ #
43
+ # This decods + to SP.
44
+ #
45
+ # See URI.encode_www_form_component, URI.decode_www_form
46
+ def self.decode_www_form_component(str, enc=nil)
47
+ if TBLDECWWWCOMP_.empty?
48
+ tbl = {}
49
+ 256.times do |i|
50
+ h, l = i>>4, i&15
51
+ tbl['%%%X%X' % [h, l]] = i.chr
52
+ tbl['%%%x%X' % [h, l]] = i.chr
53
+ tbl['%%%X%x' % [h, l]] = i.chr
54
+ tbl['%%%x%x' % [h, l]] = i.chr
55
+ end
56
+ tbl['+'] = ' '
57
+ begin
58
+ TBLDECWWWCOMP_.replace(tbl)
59
+ TBLDECWWWCOMP_.freeze
60
+ rescue
61
+ end
62
+ end
63
+ raise ArgumentError, "invalid %-encoding (#{str})" unless /\A(?:%[0-9a-fA-F]{2}|[^%])*\z/ =~ str
64
+ str.gsub(/\+|%[0-9a-fA-F]{2}/) {|m| TBLDECWWWCOMP_[m]}
65
+ end
66
+
67
+ def self.decode(value)
68
+ Supreme::URI.decode_www_form_component(value)
69
+ end
70
+
71
+ def self.encode(value)
72
+ Supreme::URI.encode_www_form_component(value).gsub("%7E", '~').gsub("+", "%20")
73
+ end
74
+
75
+ def self.parse_query(query_string)
76
+ return [] if query_string.to_s.strip == ''
77
+ query_string.strip.split('&').inject([]) do |parsed, pair|
78
+ parsed << pair.split('=', 2).map { |x| decode(x) }
79
+ end
80
+ end
81
+
82
+ def self.generate_query(pairs)
83
+ pairs.inject([]) do |parts, (key, value)|
84
+ parts << "#{encode(key)}=#{encode(value)}"
85
+ end.join('&')
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,3 @@
1
+ module Supreme
2
+ VERSION = '0.1.0'
3
+ end
metadata ADDED
@@ -0,0 +1,144 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: supreme
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Manfred Stienstra
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-02-11 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: nap
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rake
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: rdoc
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :development
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: mocha
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ type: :development
76
+ version_requirements: *id004
77
+ - !ruby/object:Gem::Dependency
78
+ name: fakeweb
79
+ prerelease: false
80
+ requirement: &id005 !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ hash: 3
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ type: :development
90
+ version_requirements: *id005
91
+ description:
92
+ email:
93
+ - manfred@fngtps.com
94
+ executables: []
95
+
96
+ extensions: []
97
+
98
+ extra_rdoc_files:
99
+ - LICENSE
100
+ - README.md
101
+ files:
102
+ - lib/supreme/api.rb
103
+ - lib/supreme/response.rb
104
+ - lib/supreme/uri.rb
105
+ - lib/supreme/version.rb
106
+ - lib/supreme.rb
107
+ - README.md
108
+ - LICENSE
109
+ has_rdoc: true
110
+ homepage: http://github.com/Fingertips/Supreme
111
+ licenses: []
112
+
113
+ post_install_message:
114
+ rdoc_options: []
115
+
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ hash: 3
124
+ segments:
125
+ - 0
126
+ version: "0"
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ none: false
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ hash: 3
133
+ segments:
134
+ - 0
135
+ version: "0"
136
+ requirements: []
137
+
138
+ rubyforge_project:
139
+ rubygems_version: 1.6.2
140
+ signing_key:
141
+ specification_version: 3
142
+ summary: Ruby implementation of the Mollie iDEAL API
143
+ test_files: []
144
+