paypal_api 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ .DS_Store
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
6
+ *~
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in paypal_api.gemspec
4
+ gemspec
data/README.markdown ADDED
@@ -0,0 +1,161 @@
1
+ # Paypal Api Gem
2
+
3
+ a ruby library to handle the entire paypal api.
4
+
5
+ paypals documentation sucks, and there do not appear to be any officially supported paypal gems.
6
+ the gems that do exist do not cover the entire api.
7
+
8
+ # Usage
9
+
10
+ ## Interfacing with the gem:
11
+ ```ruby
12
+ require "paypal_api"
13
+
14
+ request = Paypal::PaymentsPro.do_direct_payment # returns instance of Paypal::DoDirectPaymentRequest
15
+
16
+ # Set required fields
17
+ request.first_name = "mark"
18
+ request.last_name = "winton"
19
+ request.amt = 10.00
20
+
21
+ # Add a list type field
22
+ request.item.push {
23
+ :l_email => "bro@dudeman.com",
24
+ :l_amt => 23.0
25
+ }
26
+
27
+ response = request.make
28
+
29
+ response.success? # true if successful
30
+
31
+ response[:correlation_id] # correlation id string returned by paypal
32
+ response[:transaction_id] # transaction id string, not return on all calls
33
+ ```
34
+
35
+ ## Configure
36
+
37
+ ```ruby
38
+ Paypal::Request.version = "84.0"
39
+ Paypal::Request.environment = "development" # or "production"
40
+ Paypal::Request.user = "user_api1.something.com"
41
+ Paypal::Request.pwd = "some_password_they_gave_you"
42
+ Paypal::Request.signature = "some_signature"
43
+ ```
44
+
45
+ paypal api credentials for production can be found here: [https://www.paypal.com/us/cgi-bin/webscr?cmd=_profile-api-signature](https://www.paypal.com/us/cgi-bin/webscr?cmd=_profile-api-signature)
46
+
47
+ sandbox credentials can be found here: [https://developer.paypal.com/cgi-bin/devscr?cmd=_certs-session&login_access=0](https://developer.paypal.com/cgi-bin/devscr?cmd=_certs-session&login_access=0)
48
+
49
+ ## Rails
50
+
51
+ if you'd like to have multi environment configuration in rails, place a file at `config/paypal.yml` and the gem will read from it accordingly
52
+
53
+ ```yml
54
+ test:
55
+ environment: "sandbox"
56
+ username: "user_api1.something.com"
57
+ password: "some_password_they_gave_you"
58
+ signature: "some_signature"
59
+
60
+ production:
61
+ environment: "production"
62
+ username: <%= ENV["PAYPAL_USERNAME"] %>
63
+ password: <%= ENV["PAYPAL_PASSWORD"] %>
64
+ signature: <%= ENV["PAYPAL_SIGNATURE"] %>
65
+ ```
66
+
67
+ # Current Status
68
+
69
+ alpha
70
+
71
+ the work i've done so far is in order to get up and running on a project. once the project is settled, i'll be spending
72
+ some more time making the gem complete. once i feel it is in the beta stage i will push it to the official ruby gems
73
+ repository. in order to get to that stage, i will need to add support for adaptive payments, express checkout, etc...
74
+
75
+ # How To Contribute
76
+
77
+ right now the most help i could use is in writing the specs for the various api calls from the Payments Pro api. i will be working on
78
+ separating out the different access methods shortly (there's a huge difference between how to call the Payments Pro api vs the Adaptive Payments api).
79
+
80
+ as this is my first gem, i could also use help with some of the niceties with rails. ideally there will be a generator for migrating your db
81
+ to store ipn messages, and a generated class with callbacks to handle the various cases. this will probably take a lot of effort since there
82
+ are many intricacies in the meanings of the different ipn's.
83
+
84
+ for contributing to the api method request specs, look at `lib/paypal_api/apis/payments_pro.rb`
85
+
86
+ this is my first gem, so i'll be excited for any contributions :'(
87
+
88
+ # Paypal API Checklist
89
+
90
+ here's a list of api methods, and whether or not they are implemented (please take a look at `lib/paypal_api/apis/payments_pro.rb` if you'd
91
+ like to contribute, i've made it pretty easy to add compatibility for a new api call)
92
+
93
+ ## Payments Pro
94
+
95
+ * do_direct_payment - &#10003;
96
+
97
+ * do_reference_transaction - &#10003;
98
+
99
+ * do_capture - &#10003;
100
+
101
+ * do_void - &#10003;
102
+
103
+ * get_recurring_payments_profile_details - started
104
+
105
+ * address_verify
106
+
107
+ * bill_outstanding_amount
108
+
109
+ * callback
110
+
111
+ * create_recurring_payments_profile
112
+
113
+ * do_authorization
114
+
115
+ * do_express_checkout_payment
116
+
117
+ * do_nonreferenced_credit
118
+
119
+ * do_reauthorization
120
+
121
+ * get_balance
122
+
123
+ * get_billing_agreement_customer_details
124
+
125
+ * get_express_checkout_details
126
+
127
+ * get_transaction_details
128
+
129
+ * manage_pending_transaction_status
130
+
131
+ * manage_recurring_payments_profile_status
132
+
133
+ * refund_transaction
134
+
135
+ * set_customer_billing_agreement
136
+
137
+ * set_express_checkout
138
+
139
+ * transaction_search
140
+
141
+ * update_recurring_payments_profile
142
+
143
+ ## Mass Pay
144
+
145
+ note that you need to request that paypal enable mass pay for your account before it will work
146
+
147
+ * mass_pay - &#10003;
148
+
149
+ ## Instant Pay Notifications
150
+
151
+ ## Express Checkout
152
+
153
+ ## Adaptive Payments
154
+
155
+ ## Adaptive Accounts
156
+
157
+ ## Invoicing
158
+
159
+ ## Button Manager
160
+
161
+ ## Permissions
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require "paypal_api"
@@ -0,0 +1,149 @@
1
+ module Paypal
2
+ module Formatters
3
+ def escape_uri_component(string)
4
+ string = string.to_s
5
+ return URI.escape(string, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
6
+ end
7
+
8
+ def to_key(symbol)
9
+ return symbol.to_s.gsub(/[^a-z0-9]/i, "").upcase
10
+ end
11
+ end
12
+
13
+ class Api
14
+
15
+ attr_accessor :request
16
+
17
+ protected
18
+
19
+ def self.set_accessor(klass, name, type = nil)
20
+ set_reader(klass, name, type)
21
+ set_writer(klass, name, type)
22
+ end
23
+
24
+ # writers should validate before assigning value
25
+ def self.set_writer(klass, field, type)
26
+ klass.class_eval do
27
+ define_method("#{field}=") do |val|
28
+ # arbitrary value
29
+ if type.nil?
30
+ instance_variable_set("@#{field}", val)
31
+ # special types
32
+ elsif type.class == Regexp
33
+ if match = type.match(val)
34
+ instance_variable_set("@#{field}", match[0])
35
+ else
36
+ raise Paypal::InvalidParameter, "#{field} expects a string that matches #{type}"
37
+ end
38
+ elsif type.class == Optional
39
+ instance_variable_set("@#{field}", type.parse(val))
40
+ elsif type.class == Enum
41
+ instance_variable_set("@#{field}", type.parse(val))
42
+ elsif type.class == Coerce
43
+ instance_variable_set("@#{field}", type.parse(val))
44
+ elsif type.class == Default
45
+ instance_variable_set("@#{field}", type.parse(val))
46
+ # custom type
47
+ elsif type.class == Proc && type.call(val)
48
+ instance_variable_set("@#{field}", val)
49
+ # is_a type
50
+ elsif type == val.class
51
+ instance_variable_set("@#{field}",val)
52
+ else
53
+ raise Paypal::InvalidParameter, "#{field}'s spec was set incorrectly"
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def self.set_reader(klass, m, type, constant = false)
60
+ variable = "@#{m}"
61
+
62
+ klass.class_eval do
63
+ define_method(m) do
64
+ if constant
65
+ return type
66
+ elsif type.class == Default
67
+ instance_variable_set(variable, type.value) unless instance_variable_defined?(variable)
68
+ instance_variable_get(variable)
69
+ else
70
+ instance_variable_get(variable)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def self.set_sequential_reader(klass, k, v)
77
+ variable = "@#{k}"
78
+
79
+ klass.class_eval do
80
+ define_method(k) do
81
+ instance_variable_set(variable, v.clone) unless instance_variable_defined?(variable)
82
+ instance_variable_get(variable)
83
+ end
84
+ end
85
+ end
86
+
87
+ # collected from set request signature
88
+ def self.set_required(klass, keys)
89
+ klass.class_eval do
90
+ @required = keys
91
+ end
92
+ end
93
+
94
+ # collected from set request signature
95
+ def self.set_sequential(klass, keys)
96
+ klass.class_eval do
97
+ @sequential = keys
98
+ end
99
+ end
100
+
101
+ def self.symbol_to_camel(symbol)
102
+ return symbol.to_s.downcase.split("_").map(&:capitalize).join
103
+ end
104
+
105
+ def self.set_request_signature(name, hash)
106
+ # create request object
107
+ class_name = "#{self.symbol_to_camel name}Request"
108
+ self.class.class_eval <<-EOS
109
+ class Paypal::#{class_name} < Request; end
110
+ EOS
111
+ klass = Kernel.const_get("Paypal").const_get(class_name)
112
+
113
+ # add setters/getters
114
+ required = []
115
+ sequential = []
116
+ hash.each do |k,v|
117
+ if v.class == String || v.class == Fixnum || v.class == Float
118
+ set_reader klass, k, v, true
119
+ elsif v.class == Sequential
120
+ set_sequential_reader klass, k, v
121
+ else
122
+ set_accessor klass, k, v
123
+ end
124
+
125
+ required.push(k) unless v.class == Optional || v.class == Sequential
126
+ sequential.push(k) if v.class == Sequential
127
+ end
128
+
129
+ # set which keys are required for the request
130
+ set_required(klass, required)
131
+ set_sequential(klass, sequential)
132
+
133
+ # create api method
134
+ self.class_eval <<-EOS
135
+ def self.#{name}(hash = {})
136
+ return #{klass}.new(hash)
137
+ end
138
+ EOS
139
+ end
140
+
141
+ # TODO: make this useful :'(
142
+ # def set_response_signature(hash)
143
+ # hash.each do |k,v|
144
+ # set_reader k, v
145
+ # end
146
+ # end
147
+ end
148
+
149
+ end
@@ -0,0 +1,25 @@
1
+ module Paypal
2
+ class MassPay < Paypal::Api
3
+ set_request_signature :mass_pay, {
4
+ :method => "MassPay",
5
+ :email_subject => Optional.new(String), # max 255 char
6
+ :currency_code => Default.new("USD", String),
7
+ :receiver_type => Default.new("EmailAddress", Enum.new(:email_address => "EmailAddress", :user_id => "UserID")),
8
+ :payee => Sequential.new({
9
+ :email => String,
10
+ :amt => Float,
11
+ :unique_id => Optional.new(String)
12
+ }, 250, lambda {|key, i| "L_#{key.to_s.gsub("_","").upcase}#{i}"})
13
+ }
14
+ end
15
+
16
+ class MassPayRequest
17
+
18
+ protected
19
+ def validate!
20
+ if @payee.length == 0
21
+ raise Paypal::InvalidRequest, "you pust provide at least one payee"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,155 @@
1
+ module Paypal
2
+ class PaymentsPro < Paypal::Api
3
+
4
+ set_request_signature :do_direct_payment, {
5
+
6
+ # DoDirectPayment Request Fields
7
+ :method => "DoDirectPayment",
8
+ :payment_action => Optional.new(Enum.new("Authorization", "Sale")),
9
+ :ip_address => /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
10
+ :return_mf_details => Optional.new(
11
+ Coerce.new( lambda do |val|
12
+ return [1, "1", true].include?(val) ? 1 : 0
13
+ end)
14
+ ),
15
+
16
+
17
+ # Credit Card Details Fields
18
+ :credit_card_type => Optional.new(Enum.new("Visa", "MasterCard", "Discover", "Amex", "Maestro")),
19
+ :acct => /\d+/, # this should be better
20
+ :exp_date => /^\d{6}$/, # "MMYYYY"
21
+ :cvv2 => /^\d{3,4}$/,
22
+ :start_date => Optional.new(/^\d{6}$/), # maestro only
23
+ :issue_number => Optional.new(/^\d{,2}$/), #maestro only
24
+
25
+
26
+ # Payer Information Fields
27
+ :email => Optional.new(/email/), # max 127 char
28
+ :first_name => String, # max 25 char
29
+ :last_name => String, # max 25 char
30
+
31
+
32
+ # Address Fields
33
+ :street => String, # max 100 char
34
+ :street_2 => Optional.new(String), # max 100 char
35
+ :city => String, # max 40 char
36
+ :state => String, # max 40 char
37
+ :country_code => Default.new("US", /^[a-z]{2}$/i),
38
+ :zip => String, # max 20 char
39
+ :ship_to_phone_num => Optional.new(String), # max 20 char
40
+
41
+
42
+ # Payment Details Fields
43
+ :amt => Float, # complicated: https://www.x.com/developers/paypal/documentation-tools/api/dodirectpayment-api-operation-nvp
44
+ :currency_code => Default.new("USD", /^[a-z]{3}$/i),
45
+
46
+ # TODO:
47
+ :item_amt => Optional.new,
48
+ :shipping_amt => Optional.new,
49
+ :insurance_amt => Optional.new,
50
+ :shipdisc_amt => Optional.new,
51
+ :handling_amt => Optional.new,
52
+ :tax_amt => Optional.new,
53
+
54
+ :desc => Optional.new(String), # max 127 char
55
+ :custom => Optional.new(String), # max 256 char
56
+ :inv_num => Optional.new(String), # max 127 char
57
+ :button_source => Optional.new(String), # max 32 char
58
+
59
+ :notify_url => Optional.new, # hard to tell if this is part of this api or not from the wording in the docs
60
+ :recurring => Default.new("N", lambda {|anything| "Y" }),
61
+
62
+
63
+ # TODO: Payment Details Item Fields
64
+ # TODO: Ebay Item Payment Details Item Fields
65
+ # TODO: Ship To Address Fields
66
+ # TODO: 3D Secure Request Fields (U.K. Merchants Only)
67
+
68
+ }
69
+
70
+ set_request_signature :do_reference_transaction, {
71
+ :method => "DoReferenceTransaction",
72
+ :reference_id => String,
73
+ :payment_action => Default.new("Sale", Enum.new("Authorization", "Sale")),
74
+ :return_mf_details => Optional.new(
75
+ Coerce.new( lambda do |val|
76
+ return [1, "1", true].include?(val) ? 1 : 0
77
+ end)
78
+ ),
79
+ :soft_descriptor =>Optional.new(lambda {|val|
80
+ if val.match(/^([a-z0-9]|\.|-|\*| )*$/i) && val.length <= 22
81
+ return true
82
+ else
83
+ return false
84
+ end
85
+ }),
86
+
87
+ # ship to address fields
88
+ :ship_to_name => Optional.new(String), # max 32
89
+ :ship_to_street => Optional.new(String), # max 100
90
+ :ship_to_street_2 => Optional.new(String), # max 100
91
+ :ship_to_city => Optional.new(String), # max 40
92
+ :ship_to_state => Optional.new(String), # max 40
93
+ :ship_to_zip => Optional.new(String), # max 20
94
+ :ship_to_country => Optional.new(String), # max 2
95
+ :ship_to_phone_num => Optional.new(/[0-9+-]+/), # max 20
96
+
97
+ # payment details fields
98
+ :amt => Float,
99
+ :currency_code => Default.new("USD", /^[a-z]{3}$/i),
100
+
101
+ # TODO:
102
+ :item_amt => Optional.new,
103
+ :shipping_amt => Optional.new,
104
+ :insurance_amt => Optional.new,
105
+ :shipdisc_amt => Optional.new,
106
+ :handling_amt => Optional.new,
107
+ :tax_amt => Optional.new,
108
+
109
+ :desc => Optional.new(String), # max 127 char
110
+ :custom => Optional.new(String), # max 256 char
111
+ :inv_num => Optional.new(String), # max 127 char
112
+ :button_source => Optional.new(String), # max 32 char
113
+
114
+ :notify_url => Optional.new, # hard to tell if this is part of this api or not from the wording in the docs
115
+ :recurring => Default.new("N", lambda {|anything| "Y" }),
116
+
117
+ :item => Sequential.new({
118
+ :l_item_category => Enum.new("Digital", "Physical")
119
+ })
120
+
121
+ }
122
+
123
+ set_request_signature :do_capture, {
124
+ :method => "DoCapture",
125
+ :authorization_id => String, # max 19 char
126
+ :amt => Float,
127
+ :currency_code => Default.new("USD", /^[a-z]{3}$/i),
128
+ :complete_type => Default.new("Complete", Enum.new("Complete", "NotComplete")),
129
+ :inv_num => Optional.new(String), # max 127 char
130
+ :note => Optional.new(String), # max 255 char
131
+ :soft_descriptor => Optional.new(lambda {|val|
132
+ if val.match(/^([a-z0-9]|\.|-|\*| )*$/i) && val.length <= 22
133
+ return true
134
+ else
135
+ return false
136
+ end
137
+ }),
138
+
139
+ :store_id => Optional.new(String), # max 50 char
140
+ :terminal_id => Optional.new(String) # max 50 char
141
+ }
142
+
143
+ set_request_signature :do_void, {
144
+ :method => "DoVoid",
145
+ :authorization_id => String, # Note: If you are voiding a transaction that has been reauthorized, use the ID from the original authorization, and not the reauthorization.
146
+ :note => Optional.new(String) # max 255 char
147
+ }
148
+
149
+ set_request_signature :get_recurring_payments_profile_details, {
150
+ :method => "GetRecurringPaymentsProfileDetails",
151
+ :profile_id => String # max 19 char
152
+ }
153
+
154
+ end
155
+ end
@@ -0,0 +1,195 @@
1
+
2
+ module Paypal
3
+
4
+ class Api
5
+
6
+ class Parameter
7
+ attr_accessor :value
8
+
9
+ def initialize(value)
10
+ @value = value
11
+ end
12
+
13
+ def parse(anything)
14
+ @value = anything
15
+ return @value
16
+ end
17
+
18
+ def parameter_parse(val)
19
+ if @parameter.class == Class
20
+ if val.class == @parameter
21
+ return val
22
+ else
23
+ raise Paypal::InvalidParameter, "'#{val}'' is not of type #{@parameter.class}"
24
+ end
25
+ elsif @parameter.class == Regexp
26
+ match = @parameter.match(val)
27
+ if match
28
+ return match[0]
29
+ else
30
+ raise Paypal::InvalidParameter, "'#{val}' does not match #{@parameter}"
31
+ end
32
+ elsif @parameter.class < Parameter
33
+ return @parameter.parse(val)
34
+ else
35
+ raise Paypal::InvalidParameter, "#{@parameter.class} is an invalid parameter specification"
36
+ end
37
+ end
38
+ end
39
+
40
+ class Sequential < Parameter
41
+ include Paypal::Formatters
42
+
43
+ attr_accessor :list
44
+
45
+ # allows you to specify an optional key -> paypal_key proc due to nonstandard key formatting
46
+ def initialize(hash = {}, limit = nil, key_proc = nil)
47
+ @list = []
48
+ @schema = hash
49
+ @required = hash.map{|(k,v)| k if v.class != Optional }.compact
50
+ @key_proc = key_proc if key_proc
51
+ @limit = limit
52
+ end
53
+
54
+ # necessary because sequential stores request state, need a new list created for each
55
+ # request instance
56
+ def clone
57
+ return self.class.new(@schema, @limit, @key_proc)
58
+ end
59
+
60
+ def length
61
+ return @list.length
62
+ end
63
+
64
+ def push(hash)
65
+ raise Paypal::InvalidParameter, "missing required parameter for sequential field" unless (@required - hash.keys).empty?
66
+ raise Paypal::InvalidParameter, "field cannot have more than #{@limit} items, #{@list.length} provided" if !@limit.nil? && @list.length == @limit
67
+
68
+ hash.each do |k,val|
69
+ type = @schema[k]
70
+
71
+ if type.nil?
72
+ hash[k] = val
73
+ elsif type.class == Regexp
74
+ if match = type.match(val)
75
+ hash[k] = match[0]
76
+ else
77
+ raise Paypal::InvalidParameter, "'#{val}' did not match #{type}"
78
+ end
79
+ elsif [Optional, Enum, Coerce, Default].include?(type.class)
80
+ hash[k] = type.parse(val)
81
+ elsif type.class == Proc && type.call(val)
82
+ hash[k] = val
83
+ elsif type == val.class
84
+ hash[k] = val
85
+ else
86
+ raise Paypal::InvalidParameter, "#{type.class} is an invalid parameter specification"
87
+ end
88
+ end
89
+
90
+ @list.push(hash)
91
+ end
92
+
93
+ def to_key(symbol, i)
94
+ if @key_proc
95
+ return @key_proc.call(symbol, i)
96
+ else
97
+ return symbol.to_s.split("_", 2).map{|s| s.gsub("_", "") }.join("_").gsub(/[^a-z0-9_]/i, "").upcase + "#{i}"
98
+ end
99
+ end
100
+
101
+ def to_query_string
102
+ output = ""
103
+ @list.each_index do |i|
104
+ @list[i].each do |(k,v)|
105
+ output = "#{output}&#{to_key(k, i)}=#{escape_uri_component(@list[i][k])}"
106
+ end
107
+ end
108
+ return output
109
+ end
110
+
111
+ end
112
+
113
+ class Coerce < Parameter
114
+
115
+ attr_reader :method
116
+
117
+ def initialize(method)
118
+ @method = method
119
+ end
120
+
121
+ def parse(val)
122
+ return @method.call(val)
123
+ end
124
+ end
125
+
126
+ class Enum < Parameter
127
+
128
+ # needs to return the exact string if given instead of symbol
129
+ attr_reader :allowed_values
130
+
131
+ def initialize(*values)
132
+ if values.length == 1 && values[0].is_a?(::Hash)
133
+ @hash_enum = true
134
+ @allowed_values = values[0]
135
+ else
136
+ @allowed_values = values
137
+ end
138
+ end
139
+
140
+ def parse(val)
141
+ if @hash_enum
142
+ if @allowed_values.include?(val)
143
+ return @allowed_values[val]
144
+ else
145
+ raise Paypal::InvalidParameter, "'#{val}' must be a key in #{@allowed_values.keys}"
146
+ end
147
+ else
148
+ if @allowed_values.include?(normalize(val))
149
+ return normalize(val)
150
+ else
151
+ raise Paypal::InvalidParameter, "'#{val}' must be one of #{@allowed_values}"
152
+ end
153
+ end
154
+ end
155
+
156
+ def normalize(val)
157
+ return val if val.class == String
158
+ return Paypal::Api.symbol_to_camel(val) if val.class == Symbol
159
+ return nil
160
+ end
161
+ end
162
+
163
+ class Hash < Parameter
164
+
165
+ end
166
+
167
+ # Optional and Default can take other parameters as input
168
+
169
+ class Optional < Parameter
170
+ def initialize(parameter = nil)
171
+ @parameter = parameter.is_a?(Sequential) ? parameter.clone : parameter
172
+ end
173
+
174
+ def parse(val)
175
+ return parameter_parse(val)
176
+ end
177
+ end
178
+
179
+
180
+ class Default < Parameter
181
+
182
+ def initialize(value, parameter)
183
+ @value = value
184
+ @parameter = parameter.is_a?(Sequential) ? parameter.clone : parameter
185
+ end
186
+
187
+ def parse(val)
188
+ return parameter_parse(val)
189
+ end
190
+
191
+ end
192
+
193
+ end
194
+
195
+ end