elucid-adaptive_pay 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/MIT-LICENSE +20 -0
- data/README +186 -0
- data/Rakefile +39 -0
- data/VERSION +1 -0
- data/config/adaptive_pay.yml +19 -0
- data/elucid-adaptive_pay.gemspec +90 -0
- data/init.rb +1 -0
- data/install.rb +4 -0
- data/lib/adaptive_pay.rb +12 -0
- data/lib/adaptive_pay/abstract_payment_request.rb +44 -0
- data/lib/adaptive_pay/callback.rb +17 -0
- data/lib/adaptive_pay/cancel_preapproval_request.rb +16 -0
- data/lib/adaptive_pay/interface.rb +93 -0
- data/lib/adaptive_pay/parser.rb +43 -0
- data/lib/adaptive_pay/payment_request.rb +48 -0
- data/lib/adaptive_pay/preapproval_request.rb +24 -0
- data/lib/adaptive_pay/recipient.rb +17 -0
- data/lib/adaptive_pay/refund_request.rb +46 -0
- data/lib/adaptive_pay/request.rb +124 -0
- data/lib/adaptive_pay/response.rb +76 -0
- data/lib/adaptive_pay/sender.rb +13 -0
- data/spec/adaptive_pay/abstract_payment_request_spec.rb +41 -0
- data/spec/adaptive_pay/callback_spec.rb +17 -0
- data/spec/adaptive_pay/cancel_preapproval_request_spec.rb +22 -0
- data/spec/adaptive_pay/interface_spec.rb +184 -0
- data/spec/adaptive_pay/parser_spec.rb +47 -0
- data/spec/adaptive_pay/payment_request_spec.rb +53 -0
- data/spec/adaptive_pay/preapproval_request_spec.rb +9 -0
- data/spec/adaptive_pay/recipient_spec.rb +20 -0
- data/spec/adaptive_pay/refund_request_spec.rb +51 -0
- data/spec/adaptive_pay/request_spec.rb +178 -0
- data/spec/adaptive_pay/response_spec.rb +87 -0
- data/spec/adaptive_pay/sender_spec.rb +20 -0
- data/spec/fixtures/config/adaptive_pay.yml +16 -0
- data/spec/rails_mocks.rb +3 -0
- data/spec/spec_helper.rb +13 -0
- data/uninstall.rb +1 -0
- metadata +106 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
module AdaptivePay
|
2
|
+
class CancelPreapprovalRequest < Request
|
3
|
+
|
4
|
+
def self.response_type
|
5
|
+
:cancel_preapproval
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.endpoint
|
9
|
+
"CancelPreapproval"
|
10
|
+
end
|
11
|
+
|
12
|
+
attribute "requestEnvelope.errorLanguage", :default => "en_US"
|
13
|
+
attribute "preapprovalKey"
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require "erb"
|
2
|
+
|
3
|
+
module AdaptivePay
|
4
|
+
class Interface
|
5
|
+
|
6
|
+
def self.requests
|
7
|
+
@requests ||= []
|
8
|
+
end
|
9
|
+
|
10
|
+
cattr_accessor :test_response
|
11
|
+
attr_accessor :test_response
|
12
|
+
|
13
|
+
attr_accessor :base_url, :base_page_url, :environment, :username, :password, :signature, :application_id, :retain_requests_for_test
|
14
|
+
|
15
|
+
def self.test_interface(response = nil)
|
16
|
+
iface = new false
|
17
|
+
iface.retain_requests_for_test = true
|
18
|
+
iface.test_response = response
|
19
|
+
iface
|
20
|
+
end
|
21
|
+
|
22
|
+
# Initialize a new interface object, takes an optional rails_env parameter
|
23
|
+
#
|
24
|
+
# The rails_env parameter decides which configuration to load can be:
|
25
|
+
# nil -> use current Rails.env section in config/adaptive_pay.yml
|
26
|
+
# false -> dont load config from config/adaptive_pay.yml
|
27
|
+
# string/symbol -> use that section from config/adaptive_pay.yml
|
28
|
+
def initialize(rails_env=nil)
|
29
|
+
load(rails_env||Rails.env) unless rails_env == false
|
30
|
+
end
|
31
|
+
|
32
|
+
def load(rails_env)
|
33
|
+
config = YAML.load(ERB.new(File.read(File.join(Rails.root, "config/adaptive_pay.yml"))).result)[rails_env.to_s]
|
34
|
+
if config["retain_requests_for_test"] == true
|
35
|
+
@retain_requests_for_test = true
|
36
|
+
end
|
37
|
+
|
38
|
+
set_environment config.delete("environment")
|
39
|
+
config.each do |k, v|
|
40
|
+
send "#{k}=", v
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Explicitly select a paypal environment to connect to
|
45
|
+
# environment parameter can be :production, :sandbox, :beta_sandbox
|
46
|
+
def set_environment(environment)
|
47
|
+
@environment = environment.to_sym
|
48
|
+
@base_url = {
|
49
|
+
:production => "https://svcs.paypal.com/AdaptivePayments/",
|
50
|
+
:sandbox => "https://svcs.sandbox.paypal.com/AdaptivePayments/",
|
51
|
+
:beta_sandbox => "https://svcs.beta-sandbox.paypal.com/AdaptivePayments/"
|
52
|
+
}[@environment]
|
53
|
+
@base_page_url = {
|
54
|
+
:production => "https://www.paypal.com",
|
55
|
+
:sandbox => "https://www.sandbox.paypal.com",
|
56
|
+
:beta_sandbox => "https://www.beta-sandbox.paypal.com"
|
57
|
+
}[@environment]
|
58
|
+
end
|
59
|
+
|
60
|
+
def retain_requests_for_test?
|
61
|
+
!!@retain_requests_for_test
|
62
|
+
end
|
63
|
+
|
64
|
+
# send a preapproved payment request to paypal
|
65
|
+
def request_preapproval(&block)
|
66
|
+
request = PreapprovalRequest.new
|
67
|
+
yield request
|
68
|
+
perform request
|
69
|
+
end
|
70
|
+
|
71
|
+
def request_payment(&block)
|
72
|
+
request = PaymentRequest.new
|
73
|
+
yield request
|
74
|
+
perform request
|
75
|
+
end
|
76
|
+
|
77
|
+
def request_refund(&block)
|
78
|
+
request = RefundRequest.new
|
79
|
+
yield request
|
80
|
+
perform request
|
81
|
+
end
|
82
|
+
|
83
|
+
def perform(request)
|
84
|
+
if retain_requests_for_test?
|
85
|
+
self.class.requests << request
|
86
|
+
test_response || self.class.test_response
|
87
|
+
else
|
88
|
+
request.perform self
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module AdaptivePay
|
2
|
+
module Parser
|
3
|
+
# parse PayPal NVP message into nested Hash/Array structure
|
4
|
+
# * treats '.' in key as nesting level
|
5
|
+
# * treats \d+ key suffix as list index
|
6
|
+
# e.g.
|
7
|
+
# "namespace.listitem(0).key=value0&namespace.listitem(1).key=value1"
|
8
|
+
# parses to
|
9
|
+
# {"namespace" => {"listitem" => [{"key" => "value0"}, {"key" => "value1"}]}
|
10
|
+
def self.parse(body)
|
11
|
+
params = {}
|
12
|
+
body.split('&').each do |kvp|
|
13
|
+
key, value = kvp.split(/=/, 2)
|
14
|
+
|
15
|
+
# keys may be nested e.g. responseEnvelope.ack
|
16
|
+
keys = key.split('.')
|
17
|
+
value = URI.unescape(value)
|
18
|
+
|
19
|
+
# simple (non-nested, non-indexed) key
|
20
|
+
if keys.size == 1
|
21
|
+
params[keys.first] = value
|
22
|
+
else
|
23
|
+
keys, last_key = keys[0..-2], keys.last
|
24
|
+
keys.inject(params) do |a,e|
|
25
|
+
# indexed key e.g. refundInfo(0)
|
26
|
+
if m = e.match(/^(.*)\((\d+)\)$/)
|
27
|
+
key = m[1]
|
28
|
+
index = m[2].to_i
|
29
|
+
a[key] ||= []
|
30
|
+
a[key][index] ||= {}
|
31
|
+
a[key][index]
|
32
|
+
# non-indexed key
|
33
|
+
else
|
34
|
+
a[e] ||= {}
|
35
|
+
a[e]
|
36
|
+
end
|
37
|
+
end[last_key] = value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
params
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module AdaptivePay
|
2
|
+
class PaymentRequest < AbstractPaymentRequest
|
3
|
+
|
4
|
+
def self.response_type
|
5
|
+
:payment
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.endpoint
|
9
|
+
"Pay"
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :recipients
|
13
|
+
|
14
|
+
attribute "actionType", :default => "PAY"
|
15
|
+
attribute "preapprovalKey"
|
16
|
+
attribute "pin"
|
17
|
+
attribute "feesPayer"
|
18
|
+
attribute "logDefaultShippingAddress"
|
19
|
+
attribute "reverseAllParallelPaymentsOnError"
|
20
|
+
attribute "trackingId"
|
21
|
+
|
22
|
+
def initialize(&block)
|
23
|
+
@recipients = []
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_recipient(recipient_options)
|
28
|
+
if recipient_options.is_a?(AdaptivePay::Recipient)
|
29
|
+
@recipients << recipient_options
|
30
|
+
else
|
31
|
+
@recipients << AdaptivePay::Recipient.new(recipient_options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
def extra_attributes
|
37
|
+
result = {}
|
38
|
+
recipients.each_with_index do |r, i|
|
39
|
+
result["receiverList.receiver(#{i}).email"] = r.email
|
40
|
+
result["receiverList.receiver(#{i}).amount"] = sprintf "%.2f", r.amount
|
41
|
+
result["receiverList.receiver(#{i}).primary"] = r.primary?
|
42
|
+
result["receiverList.receiver(#{i}).invoiceId"] = r.invoice_id if r.invoice_id
|
43
|
+
end
|
44
|
+
super.merge(result)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module AdaptivePay
|
2
|
+
class PreapprovalRequest < AbstractPaymentRequest
|
3
|
+
|
4
|
+
def self.response_type
|
5
|
+
:preapproval
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.endpoint
|
9
|
+
"Preapproval"
|
10
|
+
end
|
11
|
+
|
12
|
+
attribute "dateOfMonth"
|
13
|
+
attribute "dayOfWeek"
|
14
|
+
attribute "endingDate", :format => :date
|
15
|
+
attribute "startingDate", :format => :date
|
16
|
+
attribute "maxAmountPerPayment"
|
17
|
+
attribute "maxNumberOfPayments"
|
18
|
+
attribute "maxNumberOfPaymentsPerPeriod"
|
19
|
+
attribute "maxTotalAmountOfAllPayments"
|
20
|
+
attribute "paymentPeriod"
|
21
|
+
attribute "pinType"
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module AdaptivePay
|
2
|
+
class RefundRequest < Request
|
3
|
+
|
4
|
+
def self.response_type
|
5
|
+
:refund
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.endpoint
|
9
|
+
"Refund"
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :recipients
|
13
|
+
|
14
|
+
attribute "requestEnvelope.errorLanguage", :default => "en_US"
|
15
|
+
attribute "currencyCode"
|
16
|
+
attribute "payKey"
|
17
|
+
attribute "trackingId"
|
18
|
+
attribute "transactionId"
|
19
|
+
attribute "ipnNotificationUrl"
|
20
|
+
|
21
|
+
def initialize(&block)
|
22
|
+
@recipients = []
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_recipient(recipient_options)
|
27
|
+
if recipient_options.is_a?(AdaptivePay::Recipient)
|
28
|
+
@recipients << recipient_options
|
29
|
+
else
|
30
|
+
@recipients << AdaptivePay::Recipient.new(recipient_options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
def extra_attributes
|
36
|
+
result = {}
|
37
|
+
recipients.each_with_index do |r, i|
|
38
|
+
result["receiverList.receiver(#{i}).email"] = r.email
|
39
|
+
result["receiverList.receiver(#{i}).amount"] = sprintf "%.2f", r.amount
|
40
|
+
result["receiverList.receiver(#{i}).primary"] = r.primary?
|
41
|
+
end
|
42
|
+
super.merge(result)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "net/https"
|
3
|
+
require 'bigdecimal/util'
|
4
|
+
|
5
|
+
module AdaptivePay
|
6
|
+
class Request
|
7
|
+
|
8
|
+
def self.response_type
|
9
|
+
:other
|
10
|
+
end
|
11
|
+
|
12
|
+
class_inheritable_accessor :attributes
|
13
|
+
self.attributes = []
|
14
|
+
|
15
|
+
def self.attribute(name, options={})
|
16
|
+
top_level_name = name.to_s.split(".").last.underscore
|
17
|
+
define_method top_level_name do
|
18
|
+
read_attribute name
|
19
|
+
end
|
20
|
+
|
21
|
+
define_method "#{top_level_name}=" do |value|
|
22
|
+
write_attribute name, value
|
23
|
+
end
|
24
|
+
|
25
|
+
self.attributes << options.merge(:name => name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.attribute_names
|
29
|
+
self.attributes.map { |a| a[:name] }
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def initialize(&block)
|
34
|
+
@attributes = {}
|
35
|
+
self.class.attributes.each do |k|
|
36
|
+
write_attribute k[:name], k[:default] if k[:default]
|
37
|
+
end
|
38
|
+
yield self if block_given?
|
39
|
+
end
|
40
|
+
|
41
|
+
def read_attribute(name)
|
42
|
+
@attributes[name]
|
43
|
+
end
|
44
|
+
|
45
|
+
def format_attribute(name)
|
46
|
+
attrib = self.class.attributes.find { |a| a[:name] == name }
|
47
|
+
case
|
48
|
+
when attrib.nil?
|
49
|
+
nil
|
50
|
+
when attrib[:format] == :date
|
51
|
+
@attributes[name].strftime("%Y-%m-%d")
|
52
|
+
else
|
53
|
+
@attributes[name]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def write_attribute(name, value)
|
58
|
+
@attributes[name] = value
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
def perform(interface)
|
63
|
+
uri = construct_uri(interface)
|
64
|
+
request = Net::HTTP::Post.new uri.request_uri
|
65
|
+
request.body = serialize
|
66
|
+
request.initialize_http_header(headers(interface))
|
67
|
+
http_response = build_http(uri).request request
|
68
|
+
Response.new interface, self.class.response_type, http_response
|
69
|
+
end
|
70
|
+
|
71
|
+
def serialize
|
72
|
+
result = []
|
73
|
+
all_attributes.each do |k, v|
|
74
|
+
# the optional second param to URI.escape defaults to URI::UNSAFE
|
75
|
+
# which doesn't match '&' as it is a valid URI character.
|
76
|
+
# however, '&' is not a valid character within NVP values.
|
77
|
+
escaped_v = URI.escape(v.to_s, /[^-_.!~*'()a-zA-Z\d;\/?:@+$#,\[\]]/n)
|
78
|
+
result << "#{k}=#{escaped_v}"
|
79
|
+
end
|
80
|
+
result.join("&")
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
def construct_uri(interface)
|
85
|
+
if interface.base_url.ends_with?("/")
|
86
|
+
URI.parse("#{interface.base_url}#{self.class.endpoint}")
|
87
|
+
else
|
88
|
+
URI.parse("#{interface.base_url}/#{self.class.endpoint}")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def build_http(uri)
|
93
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
94
|
+
http.use_ssl = (uri.port == 443)
|
95
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
96
|
+
http
|
97
|
+
end
|
98
|
+
|
99
|
+
def headers(interface)
|
100
|
+
{
|
101
|
+
"X-PAYPAL-SECURITY-USERID" => interface.username.to_s,
|
102
|
+
"X-PAYPAL-SECURITY-PASSWORD" => interface.password.to_s,
|
103
|
+
"X-PAYPAL-SECURITY-SIGNATURE" => interface.signature.to_s,
|
104
|
+
"X-PAYPAL-APPLICATION-ID" => interface.application_id.to_s,
|
105
|
+
"X-PAYPAL-REQUEST-DATA-FORMAT" => "NV",
|
106
|
+
"X-PAYPAL-RESPONSE-DATA-FORMAT" => "NV"
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
def all_attributes
|
111
|
+
result = {}
|
112
|
+
self.class.attribute_names.each do |name|
|
113
|
+
value = read_attribute(name)
|
114
|
+
result[name] = format_attribute(name) unless value.nil?
|
115
|
+
end
|
116
|
+
result.merge(extra_attributes)
|
117
|
+
end
|
118
|
+
|
119
|
+
def extra_attributes
|
120
|
+
{}
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module AdaptivePay
|
2
|
+
class Response
|
3
|
+
|
4
|
+
attr_reader :raw
|
5
|
+
|
6
|
+
def initialize(interface, type, response)
|
7
|
+
@type = type
|
8
|
+
@base_page_url = interface.base_page_url
|
9
|
+
@attributes = {}
|
10
|
+
@raw = response.body
|
11
|
+
parse response
|
12
|
+
end
|
13
|
+
|
14
|
+
def success?
|
15
|
+
!!@success
|
16
|
+
end
|
17
|
+
|
18
|
+
def failure?
|
19
|
+
!@success
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def created?
|
24
|
+
payment_exec_status == "CREATED"
|
25
|
+
end
|
26
|
+
|
27
|
+
def pending?
|
28
|
+
payment_exec_status == "PENDING"
|
29
|
+
end
|
30
|
+
|
31
|
+
def completed?
|
32
|
+
payment_exec_status == "COMPLETED"
|
33
|
+
end
|
34
|
+
|
35
|
+
def errors
|
36
|
+
read_attribute("error")
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def payment_page_url
|
41
|
+
case @type
|
42
|
+
when :preapproval
|
43
|
+
"#{@base_page_url}/webscr?cmd=_ap-preapproval&preapprovalkey=#{URI.escape(preapproval_key)}"
|
44
|
+
when :payment
|
45
|
+
"#{@base_page_url}/webscr?cmd=_ap-payment&paykey=#{URI.escape(pay_key)}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def read_attribute(name)
|
50
|
+
names = name.split('.')
|
51
|
+
names, last_name = names[0..-2], names.last
|
52
|
+
names.inject(@attributes) {|a,n| a[n] || {}}[last_name]
|
53
|
+
end
|
54
|
+
|
55
|
+
def method_missing(name, *args)
|
56
|
+
if @attributes.has_key?(name.to_s.camelize(:lower))
|
57
|
+
@attributes[name.to_s.camelize(:lower)]
|
58
|
+
else
|
59
|
+
super
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
def parse(response)
|
65
|
+
unless response.code.to_s == "200"
|
66
|
+
@success = false
|
67
|
+
return
|
68
|
+
end
|
69
|
+
|
70
|
+
@attributes = Parser.parse(response.body)
|
71
|
+
|
72
|
+
@success = %w{Success SuccessWithWarning}.include?(read_attribute("responseEnvelope.ack"))
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|