omnipay 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bd4376a27e44aa8518b5ffa2ac3d68b10b2bcc10
4
- data.tar.gz: 2ce839a8fb9322596f5b99c3bc9de877c9794583
3
+ metadata.gz: 426ecbaa0b5c6679ed1270a5f4e95d3b0e701724
4
+ data.tar.gz: d888f625b19af11cfb97ac2980187da74bcf29c8
5
5
  SHA512:
6
- metadata.gz: 0ebf71355c1f7bece7ee9b172a64d0d3266b93b8e6d2d99099d0a06849ed212b26e7795bd34a2c583635f57978fa4125f85e4dc55b74e796cc54605936c25667
7
- data.tar.gz: 27a788697beb58f2ab9243cbb1ff5f6a05298c9d1e78d19940be08495364ccb174d02bf01f6659ae7642a5e0bc65c59e01dfef57459e2e900c32ea4159f453b5
6
+ metadata.gz: e426e0bc7aeaf2da37c3fedf6ea1c42c9588f02dd3916cc86906418a14a3364c40261a18e1c3edbeffaf3f1433d11a016d9f265335829959e9e6eb5a32e98c82
7
+ data.tar.gz: 55ff42d3659b4e787a4050c9cedb41b0c2f385076517a7f1a9d3f1c5d1d36734e4c8de0a003661a5eb6096d241a5c7e4d69857950ebbb46c00107b570c9393a5
@@ -1,14 +1,17 @@
1
1
  # The root Omnipay module. Used for defining its global configuration
2
2
  module Omnipay
3
3
 
4
+ autoload :Adapter, 'omnipay/adapter'
4
5
  autoload :Gateway, 'omnipay/gateway'
5
6
  autoload :AutosubmitForm, 'omnipay/autosubmit_form'
6
7
  autoload :Configuration, 'omnipay/configuration'
7
8
  autoload :Gateways, 'omnipay/gateways'
8
9
  autoload :Middleware, 'omnipay/middleware'
10
+ autoload :Helpers, 'omnipay/helpers'
9
11
 
10
- # Error code for an untreatable response
11
- INVALID_RESPONSE = :invalid_response
12
+
13
+ # Code for a successful transaction
14
+ SUCCESS = :success
12
15
 
13
16
  # Error code for a user-initiated payment failure
14
17
  CANCELATION = :cancelation
@@ -16,18 +19,20 @@ module Omnipay
16
19
  # Error code for a valid response but a failed payment
17
20
  PAYMENT_REFUSED = :payment_refused
18
21
 
19
- # Error code for a signature mismatch
20
- WRONG_SIGNATURE = :wrong_signature
22
+ # Error code for an untreatable response
23
+ INVALID_RESPONSE = :invalid_response
21
24
 
22
25
 
23
26
 
24
27
  # Accessors to the configured gateways
28
+ # @return [Gateways] the configured gateways
25
29
  def self.gateways
26
30
  @gateways ||= Omnipay::Gateways.new
27
31
  end
28
32
 
29
33
 
30
34
  # Syntaxic sugar for adding a new gateway
35
+ # @see Gateways#push
31
36
  def self.use_gateway(opts = {}, &block)
32
37
  self.gateways.push(opts, &block)
33
38
  end
@@ -44,7 +49,7 @@ module Omnipay
44
49
  # Example use :
45
50
  #
46
51
  # Omnipay.configure do |config|
47
- # config.secret_token = "a-secret-token"
52
+ # config.base_path = '/payment'
48
53
  # end
49
54
  def self.configure
50
55
  yield configuration
@@ -0,0 +1,272 @@
1
+ module Omnipay
2
+
3
+ require 'ostruct'
4
+
5
+ # Base adapter class
6
+ # Inherited by specific implementations
7
+ class Adapter
8
+
9
+ # Handle adapter and payment configuration
10
+ # Can be defined
11
+ # - at the class level
12
+ # - at the adapter initialization
13
+ # - at the payment redirection (only payment config)
14
+ # Adapter config is mandatory
15
+ # Payment config is not, but extra fields are
16
+
17
+ ConfigField = Struct.new(:explaination, :value, :mandatory)
18
+
19
+ DEFAULT_ADAPTER_CONFIG = {
20
+ :sandbox => ConfigField.new('when enabled, the payment are not actually withdrawn', true, nil)
21
+ }
22
+
23
+ DEFAULT_PAYMENT_CONFIG = {
24
+ :amount => ConfigField.new('The amount (in cents) to pay', nil, true),
25
+ :reference => ConfigField.new('The local reference of the payment', nil, true),
26
+ :currency => ConfigField.new('The ISO 4217 code for the currency to use', 'USD', true),
27
+ :locale => ConfigField.new('The ISO 639-1 locale code for the payment page', 'en', true),
28
+ :title => ConfigField.new('The title to display on the payment page', nil, false),
29
+ :description => ConfigField.new('A description to display on the payment page', nil, false),
30
+ :template_url => ConfigField.new('The url for the template to use on the payment page', nil, false)
31
+ }
32
+
33
+
34
+ # ========================
35
+ # Setup the adapter config
36
+ # ========================
37
+
38
+ # Accessor to the adapter config
39
+ def self.adapter_config
40
+ @adapter_config ||= Omnipay::Helpers.deep_dup(DEFAULT_ADAPTER_CONFIG)
41
+ end
42
+
43
+ # Add a new config field to the adapter.
44
+ # Is mandatory and unfilled.
45
+ # If default value or optional => should be a payment config
46
+ def self.config(name, explaination)
47
+ adapter_config[name] = ConfigField.new(explaination, nil, true)
48
+ end
49
+
50
+
51
+ # ====================================
52
+ # Setup the payment redirection config
53
+ # ====================================
54
+
55
+ # Accessor to the payment config
56
+ def self.payment_config
57
+ @payment_config ||= Omnipay::Helpers.deep_dup(DEFAULT_PAYMENT_CONFIG)
58
+ end
59
+
60
+ # Add a new field to the payment config
61
+ # Options are :mandatory (default false) and :default (default nil)
62
+ def self.custom_payment_param(name, explaination, opts = {})
63
+ if payment_config.has_key?(name)
64
+ raise ArgumentError, "Cannot add custom payment param #{name}, as it already exists and is : #{payment_config[name].explaination}"
65
+ end
66
+ payment_config[name] = ConfigField.new(explaination, opts[:default], !!opts[:mandatory])
67
+ end
68
+
69
+ # Setup default values for existing payment params
70
+ def self.default_payment_params(default_values)
71
+ default_values.each do |name, default_value|
72
+ payment_config[name] && ( payment_config[name].value = default_value )
73
+ end
74
+ end
75
+
76
+
77
+ # ===================
78
+ # Setup if IPN or not
79
+ # ===================
80
+
81
+ def self.enable_ipn
82
+ @ipn_enabled = true
83
+ end
84
+
85
+ def self.ipn?
86
+ !!@ipn_enabled
87
+ end
88
+
89
+
90
+ # =============================
91
+ # Actual adapter instance logic
92
+ # =============================
93
+
94
+ def initialize(config = {})
95
+ payment_config = config.delete(:payment)
96
+
97
+ @adapter_config = build_adapter_config(config)
98
+ @payment_config = build_payment_config(payment_config)
99
+ end
100
+
101
+
102
+ # Config easy readers
103
+ # from : {:foo => ConfigField(..., value='bar')}
104
+ # to : config.foo # => 'bar'
105
+ def config
106
+ @config ||= OpenStruct.new(Hash[@adapter_config.map{|name, config_field| [name, config_field.value]}])
107
+ end
108
+
109
+ def default_payment_params
110
+ @default_payment_params ||= OpenStruct.new(Hash[@payment_config.map{|name, config_field| [name, config_field.value]}])
111
+ end
112
+
113
+
114
+ # ================
115
+ # Public interface
116
+ # ================
117
+
118
+ def request_phase(params, ipn_url, callback_url)
119
+ payment_params = Omnipay::Helpers.deep_dup(@payment_config)
120
+
121
+ # Merge params with default payment config
122
+ params.each do |name, value|
123
+ payment_params[name] && payment_params[name].value = value
124
+ end
125
+
126
+ # Validate payment params
127
+ payment_params.each do |name, config_field|
128
+ if config_field.mandatory && config_field.value.nil?
129
+ raise ArgumentError, "Mandatory payment parameter #{name} is not defined. It is supposed to be #{config_field.explaination}"
130
+ end
131
+ end
132
+
133
+ # {name => config_field} to OpenStruct(name => value)
134
+ payment_params = OpenStruct.new(
135
+ Hash[ payment_params.map{|name, config_field| [name, config_field.value]} ]
136
+ )
137
+
138
+ # Forward to the right method (redirection_with_ipn or redirection)
139
+ if self.class.ipn?
140
+ return payment_page_redirection_ipn(payment_params, ipn_url, callback_url)
141
+ else
142
+ return payment_page_redirection(payment_params, callback_url)
143
+ end
144
+ end
145
+
146
+
147
+ def ipn_hash(request)
148
+ request = Rack::Request.new(request.env.dup)
149
+ return validate_payment_notification(request)
150
+ end
151
+
152
+
153
+ def callback_hash(request)
154
+ request = Rack::Request.new(request.env.dup)
155
+ return validate_callback_status(request)
156
+ end
157
+
158
+
159
+ # ===============================
160
+ # Logic to redefine in subclasses
161
+ # ===============================
162
+
163
+ def payment_page_redirection(params, callback_url)
164
+ raise NoMethodError, "To redefine in adapter implementation"
165
+ end
166
+
167
+ def payment_page_ipn_redirection(params, ipn_url, callback_url)
168
+ raise NoMethodError, "To redefine in adapter implementation"
169
+ end
170
+
171
+
172
+ def validate_payment_notification(request)
173
+ raise NoMethodError, "To redefine in adapter implementation"
174
+ end
175
+
176
+
177
+ def validate_callback_status(request)
178
+ raise NoMethodError, "To redefine in adapter implementation"
179
+ end
180
+
181
+
182
+ protected
183
+
184
+ # ==========================
185
+ # Helpers to format response
186
+ # ==========================
187
+
188
+ def payment_error(message)
189
+ status_error(message)
190
+ end
191
+
192
+ def payment_failed(reference, reason)
193
+ status_failed(reason).merge(
194
+ :reference => reference
195
+ )
196
+ end
197
+
198
+ def payment_canceled(reference)
199
+ status_canceled.merge(
200
+ :reference => reference
201
+ )
202
+ end
203
+
204
+ def payment_successful(reference, transaction_id, amount)
205
+ status_successful(reference).merge(
206
+ :amount => amount,
207
+ :transaction_id => transaction_id
208
+ )
209
+ end
210
+
211
+
212
+ def status_error(message = '')
213
+ {:success => false, :status => Omnipay::INVALID_RESPONSE, :error_message => message}
214
+ end
215
+
216
+ def status_failed(message = '')
217
+ {:success => false, :status => Omnipay::PAYMENT_REFUSED, :error_message => message}
218
+ end
219
+
220
+ def status_canceled
221
+ {:success => false, :status => Omnipay::CANCELATION}
222
+ end
223
+
224
+ def status_successful(reference)
225
+ {:success => true, :status => Omnipay::SUCCESS, :reference => reference}
226
+ end
227
+
228
+
229
+ private
230
+
231
+ # Build the instance adapter config from the class one and a hash of value overrides
232
+ def build_adapter_config(overrides)
233
+ # Clone the default config
234
+ adapter_config = Omnipay::Helpers.deep_dup(self.class.adapter_config)
235
+
236
+ # Override its values
237
+ overrides.each do |name, value|
238
+ next unless adapter_config.has_key? name
239
+ adapter_config[name].value = value
240
+ end
241
+
242
+ # Validate it
243
+ adapter_config.each do |name, config_field|
244
+ if config_field.mandatory && config_field.value == nil
245
+ raise ArgumentError, "Mandatory config field #{name} is not defined. It is supposed to be #{config_field.explaination}"
246
+ end
247
+ end
248
+
249
+ return adapter_config
250
+ end
251
+
252
+
253
+ # Build the instance payment config from the class one and a hash of value overrides
254
+ def build_payment_config(overrides)
255
+ overrides ||= {} # Nil can be passed as an argument
256
+
257
+ # Clone the default config
258
+ payment_config = Omnipay::Helpers.deep_dup(self.class.payment_config)
259
+
260
+ # Override its values
261
+ overrides.each do |name, value|
262
+ next unless payment_config.has_key? name
263
+ payment_config[name].value = value
264
+ end
265
+
266
+ # No validation, done at the redirection time
267
+ return payment_config
268
+ end
269
+
270
+ end
271
+
272
+ end
@@ -3,11 +3,15 @@ require 'singleton'
3
3
  module Omnipay
4
4
 
5
5
  # The global Omnipay configuration singleton
6
+ # Can be assigned the following values :
7
+ # - +base_path+ (default "/pay") : the base path which will be hit in the payment providers callbacks.
8
+ # - +base_uri+ (default nil) : the base uri (scheme + host + port) which will be hit in the payment providers callbacks.
6
9
  class Configuration
7
10
  include Singleton
8
- attr_accessor :base_path
11
+ attr_accessor :base_path, :base_uri
9
12
 
10
13
  def initialize
14
+ @base_uri = nil
11
15
  @base_path = "/pay"
12
16
  end
13
17
  end
@@ -1,10 +1,18 @@
1
1
  module Omnipay
2
2
 
3
- # Instance of a gateway connection. Has an uid, and encapsulates an adapter strategy
3
+ # Instance of a gateway connection. Has an uid, and encapsulates an adapter strategy.
4
4
  class Gateway
5
5
 
6
6
  attr_reader :uid, :adapter_class, :config, :adapter
7
7
 
8
+ # @param opts [Hash]
9
+ #
10
+ # The options hash must include the following keys :
11
+ # - :uid => the gateways uid
12
+ # - :adapter => the adapter class
13
+ #
14
+ # The options hash may also include the following keys :
15
+ # - :config => the configration hash passed to the adapter for its initialization
8
16
  def initialize(opts = {})
9
17
  @uid = opts[:uid]
10
18
  @adapter_class = opts[:adapter]
@@ -18,19 +26,23 @@ module Omnipay
18
26
 
19
27
 
20
28
  # The Rack::Response corresponding to the redirection to the payment page
29
+ # @param opts [Hash] The attributes of the current payment. Will be passed on to the adapter.
30
+ # The options hash must contain the following keys :
31
+ # - +:base_uri+ : the current http scheme + host (used in the post-payment redirection)
32
+ # - +:amount [Integer]+ : the amount to pay, in cents
33
+ # Depending on the adapter used, the options hash may have other mandatory keys. Refer to the adapter's documentation for more details
34
+ # @return [Rack::Response] the GET or POST redirection to the payment provider
21
35
  def payment_redirection(opts = {})
22
- host = opts.delete :host
23
- amount = opts.delete :amount
36
+ base_uri = get_base_uri(opts)
37
+ raise ArgumentError.new('Missing parameter :base_uri') unless base_uri
24
38
 
25
- raise ArgumentError.new('Missing parameter :host') unless host
26
- raise ArgumentError.new('Missing parameter :amount') unless amount
39
+ ipn_url = "#{base_uri}#{Omnipay.configuration.base_path}/#{uid}/ipn"
40
+ callback_url = "#{base_uri}#{Omnipay.configuration.base_path}/#{uid}/callback"
27
41
 
28
- callback_url = "#{host}#{Omnipay.configuration.base_path}/#{uid}/callback"
29
-
30
- method, url, params = @adapter.request_phase(amount, callback_url, opts)
42
+ method, url, params = @adapter.request_phase(opts, ipn_url, callback_url)
31
43
 
32
44
  if method == 'GET'
33
- redirect_url = url + '?' + Rack::Utils.build_query(params)
45
+ redirect_url = url + (url.include?('?') ? '&' : '?') + Rack::Utils.build_query(params)
34
46
  Rack::Response.new.tap{|response| response.redirect(redirect_url)}
35
47
 
36
48
  elsif method == 'POST'
@@ -44,12 +56,38 @@ module Omnipay
44
56
  end
45
57
 
46
58
 
47
- # The response hash
48
- def formatted_response_for(params)
49
- @adapter.callback_hash(params).merge(:raw => params)
59
+ # The formatted response hashes
60
+ # @return [Hash] the processed response which will be present in the request environement under 'omnipay.response'
61
+ def ipn_hash(request)
62
+ @adapter.ipn_hash(request).merge(:raw => request.params)
63
+ end
64
+
65
+ def callback_hash(request)
66
+ @adapter.callback_hash(request).merge(:raw => request.params)
50
67
  end
51
68
 
52
69
 
70
+ # Is IPN enabled?
71
+ def ipn_enabled?
72
+ @adapter_class.ipn?
73
+ end
74
+
75
+
76
+ private
77
+
78
+ def get_base_uri(opts)
79
+ base_uri = opts.delete :base_uri
80
+
81
+ if !base_uri && opts[:host]
82
+ base_uri = opts.delete :host
83
+ Kernel.warn "[DEPRECATION] `host` is deprecated. Please use `base_uri` instead."
84
+ end
85
+
86
+ base_uri ||= Omnipay.configuration.base_uri
87
+
88
+ base_uri
89
+ end
90
+
53
91
  end
54
92
 
55
93
  end
@@ -3,7 +3,6 @@ module Omnipay
3
3
  # Structure responsible for storing and accessing the application's configured gateways
4
4
  class Gateways
5
5
 
6
-
7
6
  def initialize
8
7
  @gateways = {}
9
8
  @dynamic_configs = [] # Collection of procs which given a uid may or may not return a gateway config hash
@@ -11,6 +10,14 @@ module Omnipay
11
10
 
12
11
 
13
12
  # Add a new gateway, static or dynamic
13
+ #
14
+ # Can be initialized via a config hash, or a block.
15
+ #
16
+ # If initialized with a hash, it must contains the `:uid` and `:adapter` keys
17
+ #
18
+ # If initialized with a block, the block must take the uid as an argument, and return a config hash with an `:adapter` key
19
+ # @param opts [Hash] the gateway configuration, if static.
20
+ # @param block [Proc] the gateway configuration, if dynamic.
14
21
  def push(opts = {}, &block)
15
22
  if block_given?
16
23
  @dynamic_configs << Proc.new(&block)
@@ -24,7 +31,9 @@ module Omnipay
24
31
  end
25
32
 
26
33
 
27
- # Find and/or instanciate a gateway for the given uid
34
+ # Find a static gateway or instanciate a dynamic gateway for the given uid
35
+ # @param uid [String] the gateway's uid
36
+ # @return [Gateway] the corresponding gateway, or nil if none
28
37
  def find(uid)
29
38
  gateway = @gateways[uid]
30
39
  return gateway if gateway
@@ -0,0 +1,15 @@
1
+ module Omnipay
2
+
3
+ module Helpers
4
+
5
+ # Deep hash clone
6
+ def self.deep_dup(hash)
7
+ hash.inject({}) do |clone, (key, value)|
8
+ clone[key] = value.dup
9
+ clone
10
+ end
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -18,6 +18,8 @@ module Omnipay
18
18
 
19
19
  # The standard rack middleware call. Will be processed by an instance of RequestPhase or CallbackPhase if the
20
20
  # path matches the adapter's uid. Will forward to the app otherwise
21
+ # @param env [Hash] the request's environment
22
+ # @return [Rack::Reponse]
21
23
  def call(env)
22
24
 
23
25
  # Get the current request
@@ -31,16 +33,18 @@ module Omnipay
31
33
  gateway = Omnipay.gateways.find(uid)
32
34
  return @app.call(env) unless gateway
33
35
 
36
+ # Handle the IPN phase
37
+ return call_ipn(request, gateway) if ipn_phase?(request, uid)
38
+
34
39
  # Handle the callback phase
35
40
  if callback_phase?(request, uid)
36
- # Symbolize the params keys
37
- params = Hash[request.params.map{|k,v|[k.to_sym,v]}]
38
41
 
39
- # Set the formatted response
40
- request.env['omnipay.response'] = gateway.formatted_response_for(params)
42
+ # If no IPN : send the ipn request before
43
+ if !gateway.ipn_enabled?
44
+ call_ipn(request, gateway, :force => true)
45
+ end
41
46
 
42
- # Force a get request
43
- request.env['REQUEST_METHOD'] = 'GET'
47
+ return call_callback(request, gateway)
44
48
  end
45
49
 
46
50
  # Forward to the app
@@ -51,8 +55,48 @@ module Omnipay
51
55
 
52
56
  private
53
57
 
58
+ def ipn_path(uid)
59
+ "#{Omnipay.configuration.base_path}/#{uid}/ipn"
60
+ end
61
+
62
+ def callback_path(uid)
63
+ "#{Omnipay.configuration.base_path}/#{uid}/callback"
64
+ end
65
+
66
+ def ipn_phase?(request, uid)
67
+ request.path == ipn_path(uid)
68
+ end
69
+
54
70
  def callback_phase?(request, uid)
55
- request.path == "#{Omnipay.configuration.base_path}/#{uid}/callback"
71
+ request.path == callback_path(uid)
72
+ end
73
+
74
+ def call_ipn(request, gateway, opts = {})
75
+ # Force : override the path
76
+ if opts[:force]
77
+ request = Rack::Request.new(request.env.dup)
78
+ request.path_info = ipn_path(gateway.uid)
79
+ end
80
+
81
+ # Set the formatted response
82
+ request.env['omnipay.response'] = gateway.ipn_hash(request)
83
+
84
+ # Force a POST on the app
85
+ request.env['REQUEST_METHOD'] = 'POST'
86
+
87
+ # Call the app
88
+ @app.call(request.env)
89
+ end
90
+
91
+ def call_callback(request, gateway)
92
+ # Set the formatted response
93
+ request.env['omnipay.response'] = gateway.callback_hash(request)
94
+
95
+ # Force a get request
96
+ request.env['REQUEST_METHOD'] = 'GET'
97
+
98
+ # Call the app
99
+ @app.call(request.env)
56
100
  end
57
101
 
58
102
 
@@ -4,10 +4,10 @@ module Omnipay
4
4
  module Helpers
5
5
 
6
6
  def redirect_to_payment(uid, opts = {})
7
- app_host = "#{request.scheme}://#{request.host_with_port}"
7
+ base_uri = "#{request.scheme}://#{request.host_with_port}"
8
8
  gateway = Omnipay.gateways.find(uid)
9
9
  if gateway
10
- rack_response = gateway.payment_redirection(opts.merge(:host => app_host))
10
+ rack_response = gateway.payment_redirection(opts.merge(:base_uri => base_uri))
11
11
 
12
12
  self.response_body = rack_response.body
13
13
  self.status = rack_response.status
@@ -22,7 +22,11 @@ module Omnipay
22
22
  end
23
23
  end
24
24
 
25
-
25
+ # Custom helpers for rails applications
26
+ # Define the method : <b>+ActionController#redirect_to_payment(uid, opts={})+</b>
27
+ # - <b>+uid+</b> : the gateway's uid
28
+ # - <b>+opts+</b> : the options expected by Gateway#payment_redirection. The host is automatically determined, but the <b>+amount+</b> in cents, and other mandatory options depending on the adapter, have to be specified.
29
+ # Called in a controller, this method redirects the visitor to the payment provider.
26
30
  class Railtie < Rails::Railtie
27
31
 
28
32
  initializer "omnipay.configure_rails_initialization" do
@@ -0,0 +1,114 @@
1
+ module Omnipay
2
+ module Adapters
3
+
4
+ # This is a sample Omnipay adapter implementation for a fictive payment provider. You can use it as a boilerplate to develop your own.
5
+ #
6
+ # This adapter will take two arguments in its configuration :
7
+ # * *api_key* [String - mandatory] : given by the payment provider, used for authenticating to their API
8
+ # * *sandbox* [Boolean - optional] : whether the payment should be done in a sandbox or with the real payment page
9
+ # It may then be set up like this in a rack application
10
+ # Omnipay.use_gateway(
11
+ # :uid => 'my_gateway',
12
+ # :adapter => Omnipay::Adapters::SampleAdapter,
13
+ # :config => {
14
+ # :api_key => 'my-api-key',
15
+ # :sandbox => (ENV['RACK_ENV'] != 'production')
16
+ # }
17
+ # )
18
+ #
19
+ # Let's say our payment provider needs to be given the birthdate of the buyer for every payment. It also accepts a locale to display the payment page in
20
+ # It means that the redirection to the payment provider will be called like this :
21
+ # my_gateway = Omnipay.gateways.find('my_gateway')
22
+ # my_gateway.payment_redirection({
23
+ # :amount => 1295, # Amount in cents, mandatory for every adapter
24
+ # :host => 'http://www.my-website.com', # Also mandatory for every adapter, used to compute the redirect_url
25
+ # :birthdate => current_user.birthdate, # Custom field for this adapter class
26
+ # :locale => 'fr' # Optional field for this adapter class
27
+ # })
28
+ class SampleAdapter
29
+
30
+ # The adapters initialization
31
+ # @param config [Hash] the adapter's configuration which will be populated in the application's omnipay config file. For the example above in a dev environment, the value would be {:api_key => 'my-api-key', :sandbox => true}. It is up to you to decide which fields are mandatory, and to both check them in this initializer and specify them in your documentation. In this case :
32
+ # * +api_key+ : the API key given for your fictive gateway's account. Mandatory
33
+ # * +sandbox+ : whether to use a sandboxed payment page, or a real one. Optional and defaults to true
34
+ # @return [SampleAdapter]
35
+ def initialize(config = {})
36
+ @api_key = config[:api_key]
37
+ raise ArgumentError.new("Missing api_key") unless @api_key
38
+
39
+ @mode = (config[:sandbox] == false) ? 'production' : 'sandbox'
40
+ end
41
+
42
+
43
+ # Responsible for determining the redirection to the payment page for a given amount
44
+ #
45
+ # @param amount [Integer] the amount **in cents** that the user has to pay
46
+ # @param callback_url[String] where the user has be redirected after its payment
47
+ # @param params [Hash] the additional GET parameters sent to the omnipay payment URL. This is where you check wether the arguments expected by your the payment provider are present. In this case, we check :
48
+ # * +birthdate+ The user's birth date. Mandatory because expected by the provider
49
+ # * +locale+ The language of the payment page. No required
50
+ # @return [Array] an array containing the 4 following values :
51
+ # * +[String]+ 'GET' or 'POST' : the HTTP method to use for redirecting to the payment page
52
+ # * +[String]+ the absolute URL of the payment page. Example : "https\://my-provider.tld/sc12fdg57df"
53
+ # * +[Hash]+ the GET or POST parameters to send to the payment page
54
+ def request_phase(amount, callback_url, params={})
55
+ # Check our params
56
+ birthdate = params[:birthdate] && params[:birthdate].to_date
57
+ raise ArgumentError.new('parameter birthdate must be given') if birthdate.blank?
58
+
59
+ locale = params[:locale] || 'en'
60
+
61
+ # Build the transaction (this is where the provider-specific code happens)
62
+ amount_in_dollar = (amount.to_f / 100).round(2)
63
+ payment_params = build_transaction_for_provider(amount, birthdate, locale, callback_url)
64
+
65
+ # Return the redirection details (method, url, params)
66
+ return ['GET', 'http://my-provider-payment-endpoint.com', payment_params]
67
+ end
68
+
69
+
70
+ # Analyze the redirection from the payment provider, to determine if the payment is a success, and its details
71
+ # @param params [Hash] the GET/POST parameters sent by the payment gateway to the callback url
72
+ # @return [Hash] the resulting response hash which will be accessible in the application. Must contain the following values :
73
+ # * *:success* (+Boolean+) | Did the payment occur or not?
74
+ # * *:transaction_id* (+String+) <i>if successful</i> | The id of the transaction.
75
+ # * *:error* (+Symbol+) <i>if failed</i> | The reason why the payment was not successful. The available values are :
76
+ # * _Omnipay::CANCELED_ : The payment didn't occur because of the user.
77
+ # * _Omnipay::PAYMENT_REFUSED_ : The payment didn't occue because of an error on the gateway's side.
78
+ # * _Omnipay::INVALID_RESPONSE_ : The response from the gateway cannot be parsed. The payment may or may not have occured.
79
+ # * *:error_message* (+String+) <i>if failed</i> | A more detailed error message / stack trace, for logging purposes
80
+ def callback_hash(params)
81
+
82
+ transaction_id = params[:transaction_id]
83
+ transaction = fetch_transaction(transaction_id)
84
+
85
+ if !transaction
86
+ return { :success => false, :error => Omnipay::INVALID_RESPONSE, :error_message => "No transaction found with id #{transaction_id}"}
87
+ end
88
+
89
+ if transaction.success
90
+ { :success => true, :transaction_id => transaction_id }
91
+ else
92
+ if transaction.canceled
93
+ { :success => false, :error => Omnipay::CANCELATION }
94
+ else
95
+ { :success => false, :error => Omnipay::PAYMENT_REFUSED, :error_message => "Transaction #{transaction_id} was not successful : #{transaction.error}" }
96
+ end
97
+ end
98
+ end
99
+
100
+
101
+ private
102
+
103
+ def build_new_transaction(amount, locale)
104
+ # TODO
105
+ end
106
+
107
+ def fetch_transaction(transaction_id)
108
+ # TODO
109
+ end
110
+
111
+ end
112
+
113
+ end
114
+ end
@@ -1,3 +1,3 @@
1
1
  module Omnipay
2
- VERSION = "0.1.0"
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omnipay
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ClicRDV
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-01-20 00:00:00.000000000 Z
11
+ date: 2014-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -47,16 +47,15 @@ extensions: []
47
47
  extra_rdoc_files: []
48
48
  files:
49
49
  - lib/omnipay.rb
50
- - lib/omnipay/adapters/mangopay.rb
51
- - lib/omnipay/adapters/mangopay/client.rb
52
- - lib/omnipay/adapters/oneclicpay.rb
53
- - lib/omnipay/adapters/sample_adapter.rb
50
+ - lib/omnipay/adapter.rb
54
51
  - lib/omnipay/autosubmit_form.rb
55
52
  - lib/omnipay/configuration.rb
56
53
  - lib/omnipay/gateway.rb
57
54
  - lib/omnipay/gateways.rb
55
+ - lib/omnipay/helpers.rb
58
56
  - lib/omnipay/middleware.rb
59
57
  - lib/omnipay/railtie.rb
58
+ - lib/omnipay/sample_adapter.rb
60
59
  - lib/omnipay/version.rb
61
60
  homepage: https://github.com/clicrdv/omnipay
62
61
  licenses: []
@@ -77,9 +76,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
76
  version: '0'
78
77
  requirements: []
79
78
  rubyforge_project: '[none]'
80
- rubygems_version: 2.0.6
79
+ rubygems_version: 2.0.3
81
80
  signing_key:
82
81
  specification_version: 4
83
82
  summary: Payment gateway abstraction for rack applications.
84
83
  test_files: []
85
- has_rdoc:
@@ -1,138 +0,0 @@
1
- # Omnipay adapter for mangopay
2
- # documentation : http://docs.mangopay.com/api-references/
3
- #
4
- # Configuration :
5
- # - client_id:string (mandatory) : client id of your mangopay account
6
- # - client_passphrase:string (mandatory) : the passphrase for your account
7
- # - wallet_id:string (mandatory) : the wallet to be credited with the payments
8
-
9
- require 'omnipay/adapters/mangopay/client'
10
-
11
- module Omnipay
12
- module Adapters
13
-
14
- class Mangopay
15
-
16
- attr_reader :client
17
-
18
- def initialize(config = {})
19
-
20
- raise ArgumentError.new("Missing client_id, client_passphrase, or wallet_id parameter") unless [config[:client_id], config[:client_passphrase], config[:wallet_id]].all?
21
-
22
- @client = Client.new(config[:client_id], config[:client_passphrase], :sandbox => !!config[:sandbox])
23
- @wallet_id = config[:wallet_id]
24
-
25
- end
26
-
27
-
28
- def request_phase(amount, callback_url, params = {})
29
-
30
- transaction_id, redirect_url = create_web_payin(amount, callback_url, params)
31
-
32
- # Generate the path and query parameters from the returned redirect_url string
33
- uri = URI(redirect_url)
34
-
35
- return [
36
- 'GET',
37
- "#{uri.scheme}://#{uri.host}#{uri.path}",
38
- Rack::Utils.parse_nested_query(uri.query),
39
- transaction_id
40
- ]
41
-
42
- end
43
-
44
-
45
- def callback_hash(params)
46
-
47
- transaction_id = params[:transactionId]
48
-
49
- begin
50
- response = @client.get "/payins/#{transaction_id}"
51
- rescue Mangopay::Client::Error => e
52
- return {
53
- :success => false,
54
- :error => Omnipay::INVALID_RESPONSE,
55
- :error_message => "Could not fetch details of transaction #{transaction_id}"
56
- }
57
- end
58
-
59
- # Check if the response is valid
60
- if response['code'] != 200
61
- return {
62
- :success => false,
63
- :error => Omnipay::INVALID_RESPONSE
64
- }
65
- end
66
-
67
-
68
- # Successful transaction
69
- if response['Status'] == 'SUCCEEDED'
70
- {
71
- :success => true,
72
- :amount => response['DebitedFunds']['Amount'],
73
- :transaction_id => transaction_id
74
- }
75
- else
76
-
77
- # Cancelation
78
- if ['101001', '101002'].include? response['ResultCode']
79
- {
80
- :success => false,
81
- :error => Omnipay::CANCELATION
82
- }
83
- else
84
- {
85
- :success => false,
86
- :error => Omnipay::PAYMENT_REFUSED,
87
- :error_message => "Refused payment for transaction #{transaction_id}.\nCode : #{response['ResultCode']}\nMessage : #{response['ResultMessage']}"
88
- }
89
- end
90
- end
91
- end
92
-
93
-
94
- private
95
-
96
- def create_web_payin(amount, callback_url, params)
97
-
98
- # Create a user
99
- random_key = "#{Time.now.to_i}-#{(0...3).map { ('a'..'z').to_a[rand(26)] }.join}"
100
- user_params = {
101
- :Email => "user-#{random_key}@host.tld",
102
- :FirstName => "User #{random_key}",
103
- :LastName => "User #{random_key}",
104
- :Birthday => Time.now.to_i,
105
- :Nationality => "FR",
106
- :CountryOfResidence => "FR"
107
- }
108
-
109
- user_id = (@client.post('/users/natural', user_params))["Id"]
110
-
111
- # Create the web payin
112
- payin_params = {
113
- :AuthorId => user_id,
114
- :DebitedFunds => {
115
- :Currency => 'EUR',
116
- :Amount => amount
117
- },
118
- :Fees => {
119
- :Currency => 'EUR',
120
- :Amount => 0
121
- },
122
- :CreditedWalletId => @wallet_id,
123
- :ReturnURL => callback_url,
124
- :Culture => (params[:locale] || 'fr').upcase,
125
- :CardType => 'CB_VISA_MASTERCARD',
126
- :SecureMode => 'FORCE'
127
- }
128
-
129
- payin = @client.post '/payins/card/web', payin_params
130
-
131
- # Return the transaction reference, and the full redirection url
132
- return [payin["Id"], payin["RedirectURL"]]
133
- end
134
-
135
- end
136
-
137
- end
138
- end
@@ -1,71 +0,0 @@
1
- # Client for the mangopay API
2
-
3
- require 'httparty'
4
- require 'json'
5
-
6
- module Omnipay
7
- module Adapters
8
- class Mangopay
9
-
10
- class Client
11
-
12
- class Error < ::StandardError ; end
13
-
14
- include HTTParty
15
-
16
- format :json
17
-
18
- headers 'Accept' => 'application/json'
19
- headers 'Content-Type' => 'application/json'
20
-
21
- def initialize(key, secret, opts = {})
22
- @key = key
23
- @secret = secret
24
-
25
- if opts[:sandbox]
26
- @base_uri = 'https://api.sandbox.mangopay.com/v2'
27
- else
28
- @base_uri = 'https://api.mangopay.com/v2'
29
- end
30
- end
31
-
32
- def get(path)
33
- response = self.class.get "/#{@key}#{path}",
34
- :basic_auth => {:username => @key, :password => @secret},
35
- :base_uri => @base_uri
36
-
37
- check_errors(response)
38
-
39
- response.parsed_response.merge("code" => response.code)
40
- end
41
-
42
- def post(path, params = {})
43
- response = self.class.post "/#{@key}#{path}",
44
- :body => params.to_json,
45
- :basic_auth => {:username => @key, :password => @secret},
46
- :base_uri => @base_uri
47
-
48
- check_errors(response)
49
-
50
- response.parsed_response.merge("code" => response.code)
51
- end
52
-
53
-
54
- private
55
-
56
- def check_errors(response)
57
- # nocommit, log the request :/
58
- if response.code != 200
59
- error_message = response.parsed_response.merge(
60
- "code" => response.code
61
- )
62
-
63
- raise Error, error_message.inspect
64
- end
65
- end
66
-
67
- end
68
-
69
- end
70
- end
71
- end
@@ -1,132 +0,0 @@
1
- # Omnipay adapter for oneclicpay
2
- # documentation : docs.oneclicpay.com
3
- #
4
- # Configuration :
5
- # - tpe_id:string (mandatory) : serial number of your virutal payment terminal
6
- # - secret_key:string (mandatory) : security key for your account
7
- # - sandbox:boolean (default: false) : use the sandbox or the production environment
8
-
9
- require 'base64'
10
- require 'digest'
11
- require 'httparty'
12
-
13
- module Omnipay
14
- module Adapters
15
-
16
- class Oneclicpay
17
-
18
- HTTP_METHOD = 'POST'
19
-
20
- REDIRECT_URLS = {
21
- :sandbox => 'https://secure.homologation.oneclicpay.com',
22
- :production => 'https://secure.oneclicpay.com'
23
- }
24
-
25
- VALIDATION_URLS = {
26
- :sandbox => 'https://secure.homologation.oneclicpay.com:60000',
27
- :production => 'https://secure.oneclicpay.com:60000'
28
- }
29
-
30
- def initialize(config = {})
31
- @tpe_id = config[:tpe_id]
32
- @secret_key = config[:secret_key]
33
- @is_sandbox = config[:sandbox]
34
-
35
- raise ArgumentError.new("Missing tpe_id or secret_key parameter") unless [@tpe_id, @secret_key].all?
36
- end
37
-
38
-
39
- def request_phase(amount, callback_url, params={})
40
- product_name = params[:title] || ''
41
- transaction_id = params[:transaction_id] || random_transaction_id
42
- locale = params[:locale] || 'fr'
43
-
44
- [
45
- HTTP_METHOD,
46
- redirect_url,
47
- redirect_params_for(amount, product_name, transaction_id, locale, callback_url),
48
- transaction_id
49
- ]
50
- end
51
-
52
-
53
- def callback_hash(params)
54
-
55
- if params[:result] == "NOK" && params[:reason] == "Abandon de la transaction."
56
- return { :success => false, :error => Omnipay::CANCELATION }
57
- end
58
-
59
-
60
- if params[:result] == "OK"
61
-
62
- # Validate the response via the API
63
- transaction_id = params[:transactionId]
64
- amount = get_transaction_amount(transaction_id)
65
-
66
- if amount
67
- { :success => true, :amount => amount, :transaction_id => transaction_id }
68
- else
69
- { :success => false, :error => Omnipay::INVALID_RESPONSE, :error_message => "Could not fetch the amount of the transaction #{transaction_id}" }
70
- end
71
-
72
-
73
- elsif params[:result] == "NOK"
74
- { :success => false, :error => Omnipay::PAYMENT_REFUSED, :error_message => params[:reason] }
75
-
76
- else
77
- { :success => false, :error => Omnipay::INVALID_RESPONSE, :error_message => "No :result key in the params #{params.inspect}" }
78
- end
79
- end
80
-
81
-
82
- private
83
-
84
- def redirect_params_for(amount, product_name, transaction_id, locale, callback_url)
85
- {
86
- :montant => (amount.to_f/100).to_s,
87
- :idTPE => @tpe_id,
88
- :idTransaction => transaction_id,
89
- :devise => 'EUR',
90
- :lang => locale,
91
- :nom_produit => product_name,
92
- :urlRetourOK => callback_url,
93
- :urlRetourNOK => callback_url
94
- }.tap{|params|
95
- params[:sec] = signature(params)
96
- }
97
- end
98
-
99
- def random_transaction_id
100
- "#{Time.now.to_i}-#{@tpe_id}-#{random_token}"
101
- end
102
-
103
- def random_token
104
- (0...3).map { ('a'..'z').to_a[rand(26)] }.join
105
- end
106
-
107
- def signature(params)
108
- to_sign = (params.values + [@secret_key]).join('|')
109
- Digest::SHA512.hexdigest(Base64.encode64(to_sign).gsub(/\n/, ''))
110
- end
111
-
112
- def get_transaction_amount(transaction_id)
113
- response = HTTParty.post(
114
- "#{validation_url}/rest/payment/find?serialNumber=#{@tpe_id}&key=#{@secret_key}&transactionRef=#{transaction_id}",
115
- :headers => {'content-type' => "application/x-www-form-urlencoded"} # For ruby 1.8.7
116
- )
117
-
118
- (response.parsed_response["transaction"][0]["ok"] != 0) && response.parsed_response["transaction"][0]["amount"]
119
- end
120
-
121
- def redirect_url
122
- @is_sandbox ? REDIRECT_URLS[:sandbox] : REDIRECT_URLS[:production]
123
- end
124
-
125
- def validation_url
126
- @is_sandbox ? VALIDATION_URLS[:sandbox] : VALIDATION_URLS[:production]
127
- end
128
-
129
- end
130
-
131
- end
132
- end
@@ -1,115 +0,0 @@
1
- module Omnipay
2
- module Adapters
3
-
4
- # This is a sample Omnipay adapter implementation for a fictive payment gateway. You can use it as a boilerplate to develop your own.
5
- #
6
- # This adapter will take two arguments in its configuration :
7
- # * *api_key* [String - mandatory] : given by the payment gateway, used for authenticating to their API
8
- # * *sandbox* [Boolean - optional] : whether the payment should be done in a sandbox or with the real payment page
9
- # It may then be initialized like this in a rack application
10
- # use Omnipay::Gateway,
11
- # :uid => 'my-gateway',
12
- # :adapter => Omnipay::Adapters::SampleAdapter
13
- # :config => {
14
- # :api_key => 'my-api-key',
15
- # :sandbox => (ENV['RACK_ENV'] != 'production')
16
- # }
17
-
18
- class SampleAdapter
19
-
20
- # The adapters initialization
21
- # @param callback_url [String] the absolute callback URL the user should be redirected to after the payment. Will be generated by Omnipay. Example value : "http\://\www\.my-host.tld/pay/my-gateway/callback"
22
- # @param config [Hash] the adapter's configuration as defined in the application's omnipay config file. For the example above in a dev environment, the value would be {:api_key => 'my-api-key', :sandbox => true}. It is up to you to decide which fields are mandatory, and to both check them in this initializer and specify them in your documentation. In this case :
23
- # * *:api_key* [String] (mandatory) : the API key given for your fictive gateway's account
24
- # * *:sandbox* [Boolean] (optional) : whether to use a sandboxed payment page, or a real one. Defaults to true
25
- # @return [SampleAdapter]
26
- def initialize(config = {})
27
- @api_key = config[:api_key]
28
- raise ArgumentError.new("Missing api_key") unless @api_key
29
-
30
- @mode = (config[:sandbox] == false) ? 'production' : 'sandbox'
31
- end
32
-
33
-
34
- # Responsible for determining the redirection to the payment page for a given amount
35
- #
36
- # @param amount [Integer] the amount **in cents** that the user has to pay
37
- # @param params [Hash] the GET parameters sent to the omnipay payment URL. Can be used to specify transaction-specific variables (the locale to use, the payment page's title, ...). Please use the following keys if you need to, for consistency among adapters :
38
- # * *locale* The ISO 639-1 locale code for the payment page
39
- # * *title* The title to display on the payment page
40
- # * *transaction_id* The transaction id to use, if you want the user to be able to force it
41
- # @return [Array] an array containing the 4 following values :
42
- # * +[String]+ 'GET' or 'POST' : the HTTP method to use for redirecting to the payment page
43
- # * +[String]+ the absolute URL of the payment page. Example : "https\://my-provider.tld/sc12fdg57df"
44
- # * +[Hash]+ the GET or POST parameters to send to the payment page
45
- # * +[String]+ the unique transaction_id given by the payment provider for the upcoming payment. Has to be accessible in the callback phase.
46
-
47
- def request_phase(amount, calback_url, params={})
48
- amount_in_dollar = amount * 1.0 / 100
49
- locale = params[:locale] || 'en'
50
-
51
- transaction = build_new_transaction(amount_in_dollars, callback_url, locale)
52
-
53
- uri = URI(transaction.payment_url)
54
-
55
- method = 'GET'
56
- url = "#{uri.scheme}://#{uri.host}#{uri.path}"
57
- get_params = Rack::Utils.parse_query(uri.query)
58
- transaction_id = transaction.id
59
-
60
- return [
61
- method,
62
- url,
63
- get_params,
64
- transaction_id
65
- ]
66
-
67
- end
68
-
69
-
70
- # @param params [Hash] the GET/POST parameters sent by the payment gateway to the callback url
71
- # @return [Hash] the resulting response hash which will be accessible in the application. Must contain the following values :
72
- # * *:success* (+Boolean+) | Did the payment occur or not?
73
- # * *:amount* (+Integer+) <i>if successful</i> | The amount <b>in cents</b> actually payed
74
- # * *:transaction_id* (+String+) <i>if successful</i> | The id of the transaction. Must match the one returned in the request phase.
75
- # * *:error* (+Symbol+) <i>if failed</i> | The reason why the payment was not successful. The available values are :
76
- # * _Omnipay::CANCELED_ : The payment didn't occur because of the user.
77
- # * _Omnipay::PAYMENT_REFUSED_ : The payment didn't occue because of an error on the gateway's side.
78
- # * _Omnipay::INVALID_RESPONSE_ : The response from the gateway cannot be parsed. The payment may or may not have occured.
79
- # * *:error_message* (+String+) <i>if failed</i> | A more detailed error message / stack trace, for logging purposes
80
-
81
- def callback_hash(params)
82
-
83
- transaction_id = params[:transaction_id]
84
- transaction = fetch_transaction(transaction_id)
85
-
86
- if !transaction
87
- return { :success => false, :error => Omnipay::INVALID_RESPONSE, :error_message => "No transaction found with id #{transaction_id}"}
88
- end
89
-
90
- if transaction.success
91
- { :success => true, :amount => (transaction.amount*100).to_i, :transaction_id => transaction_id }
92
- else
93
- if transaction.canceled
94
- { :success => false, :error => Omnipay::CANCELATION }
95
- else
96
- { :success => false, :error => Omnipay::PAYMENT_REFUSED, :error_message => "Transaction #{transaction_id} was not successful : #{transaction.error}" }
97
- end
98
- end
99
- end
100
-
101
-
102
- private
103
-
104
- def build_new_transaction(amount, locale)
105
- # TODO
106
- end
107
-
108
- def fetch_transaction(transaction_id)
109
- # TODO
110
- end
111
-
112
- end
113
-
114
- end
115
- end