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.
Files changed (39) hide show
  1. data/.gitignore +1 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README +186 -0
  4. data/Rakefile +39 -0
  5. data/VERSION +1 -0
  6. data/config/adaptive_pay.yml +19 -0
  7. data/elucid-adaptive_pay.gemspec +90 -0
  8. data/init.rb +1 -0
  9. data/install.rb +4 -0
  10. data/lib/adaptive_pay.rb +12 -0
  11. data/lib/adaptive_pay/abstract_payment_request.rb +44 -0
  12. data/lib/adaptive_pay/callback.rb +17 -0
  13. data/lib/adaptive_pay/cancel_preapproval_request.rb +16 -0
  14. data/lib/adaptive_pay/interface.rb +93 -0
  15. data/lib/adaptive_pay/parser.rb +43 -0
  16. data/lib/adaptive_pay/payment_request.rb +48 -0
  17. data/lib/adaptive_pay/preapproval_request.rb +24 -0
  18. data/lib/adaptive_pay/recipient.rb +17 -0
  19. data/lib/adaptive_pay/refund_request.rb +46 -0
  20. data/lib/adaptive_pay/request.rb +124 -0
  21. data/lib/adaptive_pay/response.rb +76 -0
  22. data/lib/adaptive_pay/sender.rb +13 -0
  23. data/spec/adaptive_pay/abstract_payment_request_spec.rb +41 -0
  24. data/spec/adaptive_pay/callback_spec.rb +17 -0
  25. data/spec/adaptive_pay/cancel_preapproval_request_spec.rb +22 -0
  26. data/spec/adaptive_pay/interface_spec.rb +184 -0
  27. data/spec/adaptive_pay/parser_spec.rb +47 -0
  28. data/spec/adaptive_pay/payment_request_spec.rb +53 -0
  29. data/spec/adaptive_pay/preapproval_request_spec.rb +9 -0
  30. data/spec/adaptive_pay/recipient_spec.rb +20 -0
  31. data/spec/adaptive_pay/refund_request_spec.rb +51 -0
  32. data/spec/adaptive_pay/request_spec.rb +178 -0
  33. data/spec/adaptive_pay/response_spec.rb +87 -0
  34. data/spec/adaptive_pay/sender_spec.rb +20 -0
  35. data/spec/fixtures/config/adaptive_pay.yml +16 -0
  36. data/spec/rails_mocks.rb +3 -0
  37. data/spec/spec_helper.rb +13 -0
  38. data/uninstall.rb +1 -0
  39. 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,17 @@
1
+ module AdaptivePay
2
+ class Recipient
3
+
4
+ attr_accessor :email, :amount, :primary, :invoice_id
5
+
6
+ def initialize(options={})
7
+ options.each do |k, v|
8
+ send "#{k}=", v
9
+ end
10
+ end
11
+
12
+ def primary?
13
+ !!@primary
14
+ end
15
+
16
+ end
17
+ 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