fingertips-adyen 0.3.7.20100917

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ /tmp
2
+ /pkg
3
+ /doc
4
+ adyen-*.gem
5
+ .yardoc
6
+ *.swp
7
+ .DS_Store
8
+ spec/functional/initializer.rb
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 - 2009 Willem van Bergen and Michel Barbosa
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,40 @@
1
+ = Adyen
2
+
3
+ Package to simplify including the Adyen payments services into a Ruby on Rails application.
4
+
5
+ Adyen integration relies on three modes of communication between Adyen, your server and
6
+ your client/customer:
7
+
8
+ * Client-to-Adyen communication using forms and redirects.
9
+ * Adyen-to-server communications using notifications.
10
+ * Server-to-Adyen communication using SOAP services.
11
+
12
+ This library aims to ease the implementation of all these modes into your application.
13
+ Moreover, it provides matchers, assertions and mocks to make it easier to implement an
14
+ automated test suite to assert the integration is working correctly.
15
+
16
+ == Installation
17
+
18
+ Add the following line to your <tt>environment.rb</tt> and run <tt>rake gems:install</tt>
19
+ to make the Adyen functionality available in your Rails project:
20
+
21
+ config.gem 'adyen', :source => 'http://gemcutter.org
22
+
23
+ You can also install it as a Rails plugin (*deprecated*):
24
+
25
+ script/plugin install git://github.com/wvanbergen/adyen.git
26
+
27
+ == Usage
28
+
29
+ See the project wiki on http://wiki.github.com/wvanbergen/adyen to get started. Complete
30
+ RDoc documentation for the project can be found on http://rdoc.info/projects/wvanbergen/adyen.
31
+
32
+ * For more information about Adyen, see http://www.adyen.com
33
+ * For more information about integrating Adyen, see their manuals at
34
+ http://support.adyen.com/links/documentation
35
+
36
+ == About
37
+
38
+ This package is written by Michel Barbosa and Willem van Bergen for Floorplanner.com, and
39
+ made public under the MIT license (see LICENSE). It comes without warranty of any kind, so
40
+ use at your own risk.
@@ -0,0 +1,5 @@
1
+ Dir[File.dirname(__FILE__) + "/tasks/*.rake"].each { |file| load(file) }
2
+
3
+ GithubGem::RakeTasks.new(:gem)
4
+
5
+ task :default => "spec:specdoc"
@@ -0,0 +1,30 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'fingertips-adyen'
3
+ s.version = "0.3.7.20100917"
4
+ s.date = "2010-07-21"
5
+
6
+ s.summary = "Integrate Adyen payment services in you Ruby on Rails application."
7
+ s.description = <<-EOS
8
+ Package to simplify including the Adyen payments services into a Ruby on Rails application.
9
+ The package provides functionality to create payment forms, handling and storing notifications
10
+ sent by Adyen and consuming the SOAP services provided by Adyen. Moreover, it contains helper
11
+ methods, mocks and matchers to simpify writing tests/specsfor your code.
12
+ EOS
13
+
14
+ s.authors = ['Willem van Bergen', 'Michel Barbosa', 'Eloy Duran']
15
+ s.email = ['willem@vanbergen.org', 'cicaboo@gmail.com', 'eloy.de.enige@gmail.com']
16
+ s.homepage = 'http://wiki.github.com/wvanbergen/adyen'
17
+
18
+ s.add_development_dependency('rspec', '>= 1.1.4')
19
+ s.add_development_dependency('git', '>= 1.1.0')
20
+
21
+ s.requirements << 'Handsoap is required for accessing the SOAP services. See http://github.com/troelskn/handsoap.'
22
+ s.requirements << 'LibXML is required for using the RSpec matchers.'
23
+ s.requirements << 'ActiveRecord is required for storing the notifications in your database.'
24
+
25
+ s.rdoc_options << '--title' << s.name << '--main' << 'README.rdoc' << '--line-numbers' << '--inline-source'
26
+ s.extra_rdoc_files = ['README.rdoc']
27
+
28
+ s.files = %w(spec/spec_helper.rb spec/adyen_spec.rb lib/adyen/form.rb .gitignore spec/notification_spec.rb lib/adyen/api.rb LICENSE spec/api_spec.rb init.rb adyen.gemspec Rakefile spec/form_spec.rb README.rdoc lib/adyen/notification.rb lib/adyen/formatter.rb tasks/github-gem.rake lib/adyen/encoding.rb lib/adyen/matchers.rb lib/adyen.rb)
29
+ s.test_files = %w(spec/adyen_spec.rb spec/notification_spec.rb spec/api_spec.rb spec/form_spec.rb)
30
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'adyen'
@@ -0,0 +1,77 @@
1
+ # The Adyen module is the container module for all Adyen related functionality,
2
+ # which is implemented in submodules. This module only contains some global
3
+ # configuration methods.
4
+ #
5
+ # The most important submodules are:
6
+ # * {Adyen::Form} for generating payment form fields, generating redirect URLs
7
+ # to the Adyen payment system, and generating and checking of signatures.
8
+ # * {Adyen::Notification} for handling notifications sent by Adyen to your servers.
9
+ # * {Adyen::SOAP} for communicating with the Adyen SOAP services for payment
10
+ # maintenance and issuing recurring payments.
11
+ module Adyen
12
+
13
+ # Version constant for the Adyen plugin.
14
+ # DO NOT CHANGE THIS VALUE BY HAND. It will be updated automatically by
15
+ # the gem:release rake task.
16
+ VERSION = "0.3.7.20100917"
17
+
18
+ # Loads configuration settings from a Hash.
19
+ #
20
+ # @param [Hash] hash The (nested Hash) with configuration variables.
21
+ # @param [Module] mod The current working module. This parameter is used
22
+ # to recursively traverse the hash for submodules.
23
+ # @raise [StandardError] An exception is raised of an unkown configuration
24
+ # setting is encountered in the hash.
25
+ def self.load_config(hash, mod = Adyen)
26
+ hash.each do |key, value|
27
+ if key.to_s =~ /^[a-z]/ && mod.respond_to?(:"#{key}=")
28
+ mod.send(:"#{key}=", value)
29
+ elsif key.to_s =~ /^[A-Z]/
30
+ self.load_config(value, mod.const_get(key))
31
+ else
32
+ raise "Unknown configuration variable: '#{key}' for #{mod}"
33
+ end
34
+ end
35
+ end
36
+
37
+ # The Rails environment for which to use to Adyen "live" environment.
38
+ LIVE_RAILS_ENVIRONMENTS = ['production']
39
+
40
+ # Setter voor the current Adyen environment.
41
+ # @param ['test', 'live'] env The Adyen environment to use
42
+ def self.environment=(env)
43
+ @environment = env
44
+ end
45
+
46
+ # Returns the current Adyen environment, either test or live.
47
+ #
48
+ # It will return the +override+ value if set, it will return the value set
49
+ # using {Adyen.environment=} otherwise. If this value also isn't set, the
50
+ # environment is determined with {Adyen.autodetect_environment}.
51
+ #
52
+ # @param ['test', 'live'] override An environment to override the default with.
53
+ # @return ['test', 'live'] The Adyen environment that is currently being used.
54
+ def self.environment(override = nil)
55
+ override || @environment || Adyen.autodetect_environment
56
+ end
57
+
58
+ # Autodetects the Adyen environment based on the RAILS_ENV constant.
59
+ # @return ['test', 'live'] The Adyen environment that corresponds to the Rails environment
60
+ def self.autodetect_environment
61
+ (defined?(RAILS_ENV) && Adyen::LIVE_RAILS_ENVIRONMENTS.include?(RAILS_ENV.to_s.downcase)) ? 'live' : 'test'
62
+ end
63
+
64
+ # Loads submodules on demand, so that dependencies are not required.
65
+ # @param [Symbol] sym The name of the submodule
66
+ # @return [Module] The actual loaded submodule.
67
+ # @raise [LoadError, NameError] If the submodule cannot be loaded
68
+ def self.const_missing(sym)
69
+ require "adyen/#{sym.to_s.downcase}"
70
+ return Adyen.const_get(sym)
71
+ rescue Exception
72
+ super(sym)
73
+ end
74
+ end
75
+
76
+ require 'adyen/encoding'
77
+ require 'adyen/formatter'
@@ -0,0 +1,343 @@
1
+ require "net/https"
2
+
3
+ module Adyen
4
+ module API
5
+ class << self
6
+ # Username for the HTTP Basic Authentication that Adyen uses. Your username
7
+ # should be something like +ws@Company.MyAccount+
8
+ # @return [String]
9
+ attr_accessor :username
10
+
11
+ # Password for the HTTP Basic Authentication that Adyen uses. You can choose
12
+ # your password yourself in the user management tool of the merchant area.
13
+ # @return [String]
14
+ attr_accessor :password
15
+
16
+ attr_accessor :default_params
17
+ end
18
+
19
+ self.default_params = {}
20
+
21
+ #
22
+ # Shortcut methods
23
+ #
24
+
25
+ def self.authorise_payment(params = {})
26
+ PaymentService.new(params).authorise_payment
27
+ end
28
+
29
+ def self.authorise_recurring_payment(params = {})
30
+ PaymentService.new(params).authorise_recurring_payment
31
+ end
32
+
33
+ # TODO: the rest
34
+
35
+ #
36
+ # The actual classes
37
+ #
38
+
39
+ class SimpleSOAPClient
40
+ # from http://curl.haxx.se/ca/cacert.pem
41
+ CACERT = File.expand_path('../../../support/cacert.pem', __FILE__)
42
+
43
+ def self.endpoint
44
+ @endpoint ||= URI.parse(const_get('ENDPOINT_URI') % Adyen.environment)
45
+ end
46
+
47
+ attr_reader :params
48
+
49
+ def initialize(params = {})
50
+ @params = API.default_params.merge(params)
51
+ end
52
+
53
+ def call_webservice_action(action, data)
54
+ endpoint = self.class.endpoint
55
+
56
+ post = Net::HTTP::Post.new(endpoint.path, 'Accept' => 'text/xml', 'Content-Type' => 'text/xml; charset=utf-8', 'SOAPAction' => action)
57
+ post.basic_auth(API.username, API.password)
58
+ post.body = data
59
+
60
+ request = Net::HTTP.new(endpoint.host, endpoint.port)
61
+ request.use_ssl = true
62
+ request.ca_file = CACERT
63
+ request.verify_mode = OpenSSL::SSL::VERIFY_PEER
64
+
65
+ request.start do |http|
66
+ response = http.request(post)
67
+ # TODO: handle not 2xx responses
68
+ #p response
69
+ XMLQuerier.new(response.body)
70
+ end
71
+ end
72
+ end
73
+
74
+ class PaymentService < SimpleSOAPClient
75
+ ENDPOINT_URI = 'https://pal-%s.adyen.com/pal/servlet/soap/Payment'
76
+
77
+ def authorise_payment
78
+ make_payment_request(authorise_payment_request_body)
79
+ end
80
+
81
+ def authorise_recurring_payment
82
+ make_payment_request(authorise_recurring_payment_request_body)
83
+ end
84
+
85
+ private
86
+
87
+ def make_payment_request(data)
88
+ response = call_webservice_action('authorise', data)
89
+ response.xpath('//payment:authoriseResponse/payment:paymentResult') do |result|
90
+ {
91
+ :psp_reference => result.text('./payment:pspReference'),
92
+ :result_code => result.text('./payment:resultCode'),
93
+ :auth_code => result.text('./payment:authCode'),
94
+ :refusal_reason => result.text('./payment:refusalReason')
95
+ }
96
+ end
97
+ end
98
+
99
+ def authorise_payment_request_body
100
+ content = card_partial
101
+ content << RECURRING_PARTIAL if @params[:recurring]
102
+ payment_request_body(content)
103
+ end
104
+
105
+ def authorise_recurring_payment_request_body
106
+ content = RECURRING_PAYMENT_BODY_PARTIAL % (@params[:recurring_detail_reference] || 'LATEST')
107
+ payment_request_body(content)
108
+ end
109
+
110
+ def payment_request_body(content)
111
+ content << amount_partial
112
+ content << shopper_partial if @params[:shopper]
113
+ LAYOUT % [@params[:merchant_account], @params[:reference], content]
114
+ end
115
+
116
+ def amount_partial
117
+ AMOUNT_PARTIAL % @params[:amount].values_at(:currency, :value)
118
+ end
119
+
120
+ def card_partial
121
+ card = @params[:card].values_at(:holder_name, :number, :cvc, :expiry_year)
122
+ card << @params[:card][:expiry_month].to_i
123
+ CARD_PARTIAL % card
124
+ end
125
+
126
+ def shopper_partial
127
+ @params[:shopper].map { |k, v| SHOPPER_PARTIALS[k] % v }.join("\n")
128
+ end
129
+ end
130
+
131
+ class RecurringService < SimpleSOAPClient
132
+ ENDPOINT_URI = 'https://pal-%s.adyen.com/pal/servlet/soap/Recurring'
133
+
134
+ # TODO: rename to list_details and make shortcut method take the only necessary param
135
+ def list
136
+ response = call_webservice_action('listRecurringDetails', list_request_body)
137
+ response.xpath('//recurring:listRecurringDetailsResponse/recurring:result') do |result|
138
+ {
139
+ :creation_date => DateTime.parse(result.text('./recurring:creationDate')),
140
+ :details => result.xpath('.//recurring:RecurringDetail').map { |node| parse_recurring_detail(node) },
141
+ :last_known_shopper_email => result.text('./recurring:lastKnownShopperEmail'),
142
+ :shopper_reference => result.text('./recurring:shopperReference')
143
+ }
144
+ end
145
+ end
146
+
147
+ private
148
+
149
+ def list_request_body
150
+ LAYOUT % [@params[:merchant_account], @params[:shopper][:reference]]
151
+ end
152
+
153
+ # @todo add support for elv
154
+ def parse_recurring_detail(node)
155
+ result = {
156
+ :recurring_detail_reference => node.text('./recurring:recurringDetailReference'),
157
+ :variant => node.text('./recurring:variant'),
158
+ :creation_date => DateTime.parse(node.text('./recurring:creationDate'))
159
+ }
160
+
161
+ card = node.xpath('./recurring:card')
162
+ if card.children.empty?
163
+ result[:bank] = parse_bank_details(node.xpath('./recurring:bank'))
164
+ else
165
+ result[:card] = parse_card_details(card)
166
+ end
167
+
168
+ result
169
+ end
170
+
171
+ def parse_card_details(card)
172
+ {
173
+ :expiry_date => Date.new(card.text('./payment:expiryYear').to_i, card.text('./payment:expiryMonth').to_i, -1),
174
+ :holder_name => card.text('./payment:holderName'),
175
+ :number => card.text('./payment:number')
176
+ }
177
+ end
178
+
179
+ def parse_bank_details(bank)
180
+ {
181
+ :bank_account_number => bank.text('./payment:bankAccountNumber'),
182
+ :bank_location_id => bank.text('./payment:bankLocationId'),
183
+ :bank_name => bank.text('./payment:bankName'),
184
+ :bic => bank.text('./payment:bic'),
185
+ :country_code => bank.text('./payment:countryCode'),
186
+ :iban => bank.text('./payment:iban'),
187
+ :owner_name => bank.text('./payment:ownerName')
188
+ }
189
+ end
190
+ end
191
+
192
+ class XMLQuerier
193
+ NS = {
194
+ 'payment' => 'http://payment.services.adyen.com',
195
+ 'recurring' => 'http://recurring.services.adyen.com',
196
+ 'common' => 'http://common.services.adyen.com'
197
+ }
198
+
199
+ class << self
200
+ attr_accessor :backend
201
+
202
+ def backend=(backend)
203
+ @backend = backend
204
+ class_eval do
205
+ private
206
+ if backend == :nokogiri
207
+ def document_for_xml(xml)
208
+ Nokogiri::XML::Document.parse(xml)
209
+ end
210
+ def perform_xpath(query)
211
+ @node.xpath(query, NS)
212
+ end
213
+ else
214
+ def document_for_xml(xml)
215
+ REXML::Document.new(xml)
216
+ end
217
+ def perform_xpath(query)
218
+ REXML::XPath.match(@node, query, NS)
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ begin
226
+ require 'nokogiri'
227
+ self.backend = :nokogiri
228
+ rescue LoadError
229
+ require 'rexml/document'
230
+ self.backend = :rexml
231
+ end
232
+
233
+ def initialize(data)
234
+ @node = data.is_a?(String) ? document_for_xml(data) : data
235
+ end
236
+
237
+ def xpath(query)
238
+ result = self.class.new(perform_xpath(query))
239
+ block_given? ? yield(result) : result
240
+ end
241
+
242
+ def text(query)
243
+ xpath("#{query}/text()").to_s
244
+ end
245
+
246
+ def children
247
+ @node.first.children
248
+ end
249
+
250
+ def empty?
251
+ @node.empty?
252
+ end
253
+
254
+ def to_s
255
+ @node.to_s
256
+ end
257
+
258
+ def map(&block)
259
+ @node.map { |n| self.class.new(n) }.map(&block)
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ ########################
266
+ #
267
+ # XML template constants
268
+ #
269
+ ########################
270
+
271
+ module Adyen
272
+ module API
273
+ class PaymentService
274
+ LAYOUT = <<EOS
275
+ <?xml version="1.0"?>
276
+ <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
277
+ <soap:Body>
278
+ <ns1:authorise xmlns:ns1="http://payment.services.adyen.com">
279
+ <ns1:paymentRequest>
280
+ <merchantAccount xmlns="http://payment.services.adyen.com">%s</merchantAccount>
281
+ <reference xmlns="http://payment.services.adyen.com">%s</reference>
282
+ %s
283
+ </ns1:paymentRequest>
284
+ </ns1:authorise>
285
+ </soap:Body>
286
+ </soap:Envelope>
287
+ EOS
288
+
289
+ AMOUNT_PARTIAL = <<EOS
290
+ <amount xmlns="http://payment.services.adyen.com">
291
+ <currency xmlns="http://common.services.adyen.com">%s</currency>
292
+ <value xmlns="http://common.services.adyen.com">%s</value>
293
+ </amount>
294
+ EOS
295
+
296
+ CARD_PARTIAL = <<EOS
297
+ <card xmlns="http://payment.services.adyen.com">
298
+ <holderName>%s</holderName>
299
+ <number>%s</number>
300
+ <cvc>%s</cvc>
301
+ <expiryYear>%s</expiryYear>
302
+ <expiryMonth>%02d</expiryMonth>
303
+ </card>
304
+ EOS
305
+
306
+ RECURRING_PARTIAL = <<EOS
307
+ <recurring xmlns="http://recurring.services.adyen.com">
308
+ <contract xmlns="http://payment.services.adyen.com">RECURRING</contract>
309
+ </recurring>
310
+ EOS
311
+
312
+ RECURRING_PAYMENT_BODY_PARTIAL = RECURRING_PARTIAL + <<EOS
313
+ <ns1:selectedRecurringDetailReference>%s</ns1:selectedRecurringDetailReference>
314
+ <ns1:shopperInteraction>ContAuth</ns1:shopperInteraction>
315
+ EOS
316
+
317
+ SHOPPER_PARTIALS = {
318
+ :reference => ' <shopperReference xmlns="http://payment.services.adyen.com">%s</shopperReference>',
319
+ :email => ' <shopperEmail xmlns="http://payment.services.adyen.com">%s</shopperEmail>',
320
+ :ip => ' <shopperIP xmlns="http://payment.services.adyen.com">%s</shopperIP>',
321
+ }
322
+ end
323
+
324
+ class RecurringService
325
+ LAYOUT = <<EOS
326
+ <?xml version="1.0"?>
327
+ <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
328
+ <soap:Body>
329
+ <ns1:listRecurringDetails xmlns:ns1="http://recurring.services.adyen.com">
330
+ <ns1:request>
331
+ <ns1:recurring>
332
+ <ns1:contract>RECURRING</ns1:contract>
333
+ </ns1:recurring>
334
+ <ns1:merchantAccount>%s</ns1:merchantAccount>
335
+ <ns1:shopperReference>%s</ns1:shopperReference>
336
+ </ns1:request>
337
+ </ns1:listRecurringDetails>
338
+ </soap:Body>
339
+ </soap:Envelope>
340
+ EOS
341
+ end
342
+ end
343
+ end