elucid-adaptive_pay 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|