ez_paypal 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,32 @@
1
+ lib = File.expand_path("../lib/", __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+
4
+ require "date"
5
+ require "ez_paypal/version"
6
+
7
+ Gem::Specification.new do |s|
8
+
9
+ # Basic info
10
+ s.name = "ez_paypal"
11
+ s.version = EZPaypal::VERSION
12
+ s.platform = Gem::Platform::RUBY
13
+ s.date = Date.today.to_s
14
+ s.summary = "Paypal express checkout plugin"
15
+ s.description = "Paypal express checkout plugin"
16
+ s.authors = ["Tianyu Huang"]
17
+ s.email = ["tianhsky@yahoo.com"]
18
+ s.homepage = "http://rubygems.org/gems/ez_paypal"
19
+
20
+ # Dependencies
21
+ #s.required_rubygems_version = ">= 1.8.22"
22
+ s.add_dependency "ez_http", ">= 1.0.4"
23
+ s.add_dependency "json", ">= 1.6.6"
24
+ s.add_dependency "activesupport", ">= 3.2.2"
25
+
26
+ # Files
27
+ s.files = `git ls-files`.split("\n")
28
+ s.require_paths = ["lib"]
29
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
30
+ s.extra_rdoc_files = ["README.md", "doc/index.html"]
31
+
32
+ end
@@ -0,0 +1,55 @@
1
+ module EZPaypal
2
+ module Config
3
+
4
+ # Account setting
5
+ @setting
6
+
7
+ def self.Setting
8
+ @setting
9
+ end
10
+
11
+ # API endpoint
12
+ @endpoint
13
+
14
+ def self.EndPoint
15
+ @endpoint
16
+ end
17
+
18
+ # Setup config
19
+ # @param [Hash] options = { "user", "password", "signature",
20
+ # "version", "mode" => "sandbox / live",
21
+ # "customer_service_tel"}
22
+ #
23
+ def self.Setup (options)
24
+ # Setup account setting
25
+ @setting={
26
+ "USER" => options["user"],
27
+ "PWD" => options["password"],
28
+ "SIGNATURE" => options["signature"],
29
+ "VERSION" => options["version"] || 84.0,
30
+ "CUSTOMERSERVICENUMBER" => options["customer_service_tel"]
31
+ }
32
+
33
+ # Setup endpoint
34
+ express_checkout_endpoint = {
35
+ "sandbox" => "https://api-3t.sandbox.paypal.com/nvp",
36
+ "live" => "https://api-3t.paypal.com/nvp"
37
+ }
38
+ set_express_checkout = {
39
+ "sandbox" => "https://www.sandbox.paypal.com/webscr?cmd=_express-checkout",
40
+ "live" => "https://www.paypal.com/webscr?cmd=_express-checkout",
41
+ }
42
+
43
+ @endpoint ||= {}
44
+ if (options["mode"] == 'sandbox')
45
+ @endpoint.merge!("express_checkout_endpoint" => express_checkout_endpoint["sandbox"])
46
+ @endpoint.merge!("express_checkout_url" => set_express_checkout["sandbox"])
47
+ else
48
+ @endpoint.merge!("express_checkout_endpoint" => express_checkout_endpoint["live"])
49
+ @endpoint.merge!("express_checkout_url" => set_express_checkout["live"])
50
+ end
51
+
52
+
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,241 @@
1
+ require 'active_support/hash_with_indifferent_access'
2
+ require 'cgi'
3
+
4
+ module EZPaypal
5
+
6
+ # Paypal NVP Documentation:
7
+ # Express checkout:
8
+ # https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_nvp_r_GetExpressCheckoutDetails
9
+ # Recurring payments:
10
+ # https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_ECRecurringPayments
11
+ # https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_nvp_r_CreateRecurringPayments
12
+ module Cart
13
+
14
+ # A cart object that holds all one time purchase items
15
+ # How to use:
16
+ # -> initialize Cart
17
+ # -> add items (optional)
18
+ # -> setup shipping info(optional)
19
+ # -> setup summary(required)
20
+ # -> done (if involve recurring purchase, please create profile, this cart only shows agreement for recurring items)
21
+ # Note:
22
+ # you are responsible for all the cost calculations
23
+ #
24
+ class OneTimePurchaseCart < HashWithIndifferentAccess
25
+
26
+ # Max cart size of this cart type
27
+ def cartSize
28
+ 10
29
+ end
30
+
31
+ # Add item to the cart (optional)
32
+ # @param [Hash] item = {"category" => "digital / physical",
33
+ # "name", "item_code", "description", "amount", "quantity" }
34
+ #
35
+ def addItem(item)
36
+
37
+ # Check max cart size, make sure do not exceed the limit
38
+ max = 0
39
+ self.each do |key, value|
40
+ max = max + 1 if key.match(/^L_PAYMENTREQUEST_0_NAME/)
41
+ end
42
+ throw "Exceed max cart size: #{cartSize}" if (max+1 > cartSize)
43
+
44
+ # Add item to cart
45
+ current_index = max
46
+ current_item = {
47
+ "L_PAYMENTREQUEST_0_ITEMCATEGORY#{current_index}" => item["category"] || "Physical",
48
+ "L_PAYMENTREQUEST_0_NAME#{current_index}" => item["name"] || "",
49
+ "L_PAYMENTREQUEST_0_NUMBER#{current_index}" => item["item_code"] || "",
50
+ "L_PAYMENTREQUEST_0_DESC#{current_index}" => item["description"] || "",
51
+ "L_PAYMENTREQUEST_0_AMT#{current_index}" => item["amount"] || "0",
52
+ "L_PAYMENTREQUEST_0_QTY#{current_index}" => item["quantity"] || "0"
53
+ }
54
+ self.merge!(current_item)
55
+
56
+ end
57
+
58
+ # Setup shipping info (optional)
59
+ # @param [Hash] shipping = {"name", "street", "street2", "city", "state", "country", "zip", "phone" }
60
+ def setupShippingInfo(shipping)
61
+ item = shipping
62
+ options = {
63
+ "PAYMENTREQUEST_0_SHIPTONAME" => item["name"] || "",
64
+ "PAYMENTREQUEST_0_SHIPTOSTREET" => item["street"] || "",
65
+ "PAYMENTREQUEST_0_SHIPTOSTREET2" => item["street2"] || "",
66
+ "PAYMENTREQUEST_0_SHIPTOCITY" => item["city"] || "",
67
+ "PAYMENTREQUEST_0_SHIPTOSTATE" => item["state"] || "",
68
+ "PAYMENTREQUEST_0_SHIPTOCOUNTRYCODE" => item["country"] || "",
69
+ "PAYMENTREQUEST_0_SHIPTOZIP" => item["zip"] || "",
70
+ "PAYMENTREQUEST_0_SHIPTOPHONENUM" => item["phone"] || ""
71
+ }
72
+ self.merge!(options)
73
+ end
74
+
75
+ # Add cart summary, please calculate yourself (required)
76
+ # @param [Hash] summary = {"currency" => "USD"(default "USD"),
77
+ # "subtotal", "tax", "shipping", "handling", "shipping_discount", "insurance", "total",
78
+ # "disable_change_shipping_info" => "1 / 0" (default 1),
79
+ # "allow_note" => "1 / 0" (default 0) }
80
+ #
81
+ def setupSummary (summary)
82
+ item = summary
83
+ options = {
84
+ "PAYMENTREQUEST_0_PAYMENTACTION" => item["payment_action"] || "Sale",
85
+ "PAYMENTREQUEST_0_CURRENCYCODE" => item["currency"] || "USD",
86
+
87
+ "PAYMENTREQUEST_0_ITEMAMT" => item["subtotal"] || "0",
88
+ "PAYMENTREQUEST_0_TAXAMT" => item["tax"] || "0",
89
+ "PAYMENTREQUEST_0_SHIPPINGAMT" => item["shipping"] || "0",
90
+ "PAYMENTREQUEST_0_HANDLINGAMT" => item["handling"] || "0",
91
+ "PAYMENTREQUEST_0_SHIPDISCAMT" => item["shipping_discount"] || "0",
92
+ "PAYMENTREQUEST_0_INSURANCEAMT" => item["insurance"] || "0",
93
+ "PAYMENTREQUEST_0_AMT" => item["total"] || "0",
94
+
95
+ "ALLOWNOTE" => item["allow_note"] || "0",
96
+ "NOSHIPPING" => item["disable_change_shipping_info"] || "1"
97
+ }
98
+ self.merge!(options)
99
+ end
100
+
101
+ # Clean the cart
102
+ def reset
103
+ self.clear
104
+ end
105
+
106
+ end
107
+
108
+
109
+ # A cart object that holds all recurring purchase items
110
+ # Note that this cart only support one subscription item at a time
111
+ # How to use:
112
+ # -> initialize Cart
113
+ # -> setup agreement (required)
114
+ # -> setup summary(required)
115
+ # -> done (please create profile after this, this cart only shows agreement for recurring items)
116
+ # Note:
117
+ # you are responsible for all the cost calculations
118
+ #
119
+ class RecurringPurchaseCart < HashWithIndifferentAccess
120
+
121
+ # Max cart size of this cart type
122
+ def cartSize
123
+ 1
124
+ end
125
+
126
+ # Setup recurring payment agreement
127
+ # @param [Hash] agreement = {"category" => "digital / physical" (default physical),
128
+ # "currency" => "USD"(default "USD")
129
+ # "item_code" => ""(code should be unique and meaningful),
130
+ # "unit_price", "quantity" ,"amount"}
131
+ #
132
+ def setupAgreement(agreement)
133
+
134
+ # Check max cart size, make sure do not exceed the limit
135
+ max = 0
136
+ self.each do |key, value|
137
+ max = max + 1 if key.match(/^L_PAYMENTREQUEST_0_NAME/)
138
+ end
139
+ throw "Exceed max cart size: #{cartSize}" if (max+1 > cartSize)
140
+
141
+ item = agreement
142
+ current_index = max
143
+ current_item = {
144
+ "L_PAYMENTREQUEST_0_NAME#{current_index}" => item["item_code"] || "",
145
+ #"L_PAYMENTREQUEST_0_NUMBER#{current_index}" => item["item_code"] || "",
146
+ #"L_PAYMENTREQUEST_0_DESC#{current_index}" => item["description"] || "",
147
+ "L_PAYMENTREQUEST_0_AMT#{current_index}" => item["unit_price"] || "0",
148
+ "L_PAYMENTREQUEST_0_QTY#{current_index}" => item["quantity"] || "0",
149
+
150
+ "L_PAYMENTREQUEST_0_ITEMCATEGORY#{current_index}" => item["category"] || "Physical",
151
+ "L_BILLINGTYPE#{current_index}" => "RecurringPayments",
152
+ "L_BILLINGAGREEMENTDESCRIPTION#{current_index}" => item["item_code"] || ""
153
+ }
154
+
155
+ summary = {
156
+ "PAYMENTREQUEST_0_PAYMENTACTION" => item["payment_action"] || "Sale",
157
+ "PAYMENTREQUEST_0_CURRENCYCODE" => item["currency"] || "USD",
158
+ #"PAYMENTREQUEST_0_ITEMAMT" => item["total"] || "0",
159
+ #"PAYMENTREQUEST_0_TAXAMT" => item["tax"] || "0",
160
+ #"PAYMENTREQUEST_0_SHIPPINGAMT" => item["shipping"] || "0",
161
+ #"PAYMENTREQUEST_0_HANDLINGAMT" => item["handling"] || "0",
162
+ #"PAYMENTREQUEST_0_SHIPDISCAMT" => item["shipping_discount"] || "0",
163
+ #"PAYMENTREQUEST_0_INSURANCEAMT" => item["insurance"] || "0",
164
+ "PAYMENTREQUEST_0_AMT" => item["amount"] || "0",
165
+
166
+ "ALLOWNOTE" => item["allow_note"] || "0",
167
+ "NOSHIPPING" => item["disable_change_shipping_info"] || "1"
168
+ }
169
+ self.merge!(current_item).merge!(summary)
170
+ end
171
+
172
+ # Clean the cart
173
+ def reset
174
+ self.clear
175
+ end
176
+ end
177
+
178
+
179
+ class RecurringProfile < HashWithIndifferentAccess
180
+
181
+ # Setup profile
182
+ # @param [Hash] profile = {"token", "email", "currency" => "USD" (default "USD"),
183
+ # "item_code", "unit_price", "quantity", "amount",
184
+ # "initial_amount" => "0" (recurring profile will do auto-charge since begining! do not use this if you are not sure),
185
+ # "start_date" => "2012/02/02" (Default Time.now),
186
+ # "period" => "Day/Week/Month/Year" (default "Month"),
187
+ # "frequency" => "1" (how many periods a purchase, default 1),
188
+ # "cycles" => "0" (total cycles of recurring billing, default 0 means infinite)
189
+ # }
190
+ def initialize(profile)
191
+ item = profile
192
+ options = {
193
+ "TOKEN" => item["token"],
194
+ "EMAIL" => item["email"],
195
+ "CURRENCYCODE" => item["currency"] || "USD",
196
+ "FAILEDINITAMTACTION" => "CancelOnFailure", #ContinueOnFailure / CancelOnFailure
197
+
198
+ # Display details
199
+ "L_PAYMENTREQUEST_0_ITEMCATEGORY0" => item["category"] || "Physical",
200
+ "L_PAYMENTREQUEST_0_NAME0" => item["item_code"] || "",
201
+ "L_PAYMENTREQUEST_0_AMT0" => item["unit_price"] || "0",
202
+ "L_PAYMENTREQUEST_0_QTY0" => item["quantity"] || "0",
203
+
204
+ # Profile details
205
+ "DESC" => item["item_code"] || "",
206
+ "AMT" => item["amount"] || "0",
207
+ "QTY" => item["quantity"] || "0",
208
+ "INITAMT" => item["initial_amount"] || "0",
209
+
210
+ # Recurring details
211
+ "PROFILESTARTDATE" => item["start_date"] || Time.now,
212
+ "BILLINGFREQUENCY" => item["frequency"] || "1",
213
+ "BILLINGPERIOD" => item["period"] || "Month",
214
+ "TOTALBILLINGCYCLES" => item["cycles"] || "0"
215
+
216
+ }
217
+
218
+ self.merge!(options)
219
+ end
220
+
221
+ def self.ConvertFromCheckoutDetails (checkout_details)
222
+ profile = {
223
+ "token" => checkout_details["TOKEN"],
224
+ "email" => checkout_details["EMAIL"],
225
+ "item_code" => checkout_details["L_PAYMENTREQUEST_0_NAME0"],
226
+ "unit_price" => checkout_details["L_PAYMENTREQUEST_0_AMT0"],
227
+ "quantity" => checkout_details["L_PAYMENTREQUEST_0_QTY0"],
228
+ "amount" => checkout_details["PAYMENTREQUEST_0_AMT"],
229
+ "start_date" => Time.now,
230
+ "period" => "Month",
231
+ "frequency" => "1"
232
+ }
233
+
234
+ profile = self.new(profile)
235
+ end
236
+
237
+ end
238
+
239
+
240
+ end
241
+ end
@@ -0,0 +1,34 @@
1
+ require 'active_support/hash_with_indifferent_access'
2
+ require 'cgi'
3
+
4
+ module EZPaypal
5
+ module Helper
6
+ # Helper method to convert query string to hash
7
+ # also convert hash to encoded hash
8
+ # @param [String / Hash] object to be converted or encoded
9
+ # @return [Hash] converted string or encoded hash
10
+ def self.ConvertParamToHash (object)
11
+ hash_obj = HashWithIndifferentAccess.new()
12
+ if (object.class.to_s == "String")
13
+ object.split("&").each do |e|
14
+ key = CGI::unescape(e.split("=")[0])
15
+ value = CGI::unescape(e.split("=")[1])
16
+ hash_obj.merge!(key => value)
17
+ end
18
+ else
19
+ object.each do |key, value|
20
+ key = CGI::unescape(key)
21
+ value = CGI::unescape(value)
22
+ hash_obj.merge!(key.upcase => value)
23
+ end
24
+ end
25
+
26
+ return hash_obj
27
+ end
28
+
29
+ def self.ConvertHashToQueryString (hash)
30
+ hash.map { |k, v| CGI::escape(k.to_s)+"="+CGI::escape(v.to_s) }.join("&")
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,159 @@
1
+ require 'active_support/hash_with_indifferent_access'
2
+ require 'cgi'
3
+ require 'ez_http'
4
+ require File.expand_path('../../config', __FILE__)
5
+ require File.expand_path('../helper', __FILE__)
6
+
7
+ module EZPaypal
8
+ module Request
9
+
10
+ # Setup express checkout cart
11
+ # @param [EZPaypal::Cart::OneTimePurchaseCart / EZPaypal::Cart::RecurringPurchaseCart] cart
12
+ # @param [String] returnUrl
13
+ # @param [String] cancelUrl
14
+ # @return [Hash] response = {"TOKEN"}
15
+ def self.SetupExpressCheckout (cart, returnUrl, cancelUrl)
16
+ begin
17
+ # Setup default options
18
+ setting = EZPaypal::Config.Setting.clone
19
+ default = {"METHOD" => "SetExpressCheckout",
20
+ "RETURNURL" => returnUrl,
21
+ "CANCELURL" => cancelUrl
22
+ }
23
+ setting.merge!(default).merge!(cart)
24
+
25
+ # Get setup express checkout details
26
+ query_string = EZPaypal::Helper.ConvertHashToQueryString(setting)
27
+ setup_ec_response_origin = EZHttp.Send(EZPaypal::Config.EndPoint["express_checkout_endpoint"], query_string, "post").body
28
+
29
+ # Convert express checkout details to Hash to return
30
+ setup_ec_response = EZPaypal::Helper.ConvertParamToHash(setup_ec_response_origin)
31
+
32
+ return setup_ec_response
33
+ rescue
34
+ throw "Error occured in SetupExpressCheckout"
35
+ end
36
+ end
37
+
38
+ # Get current checkout url to redirect user to, only works if token has obtained
39
+ # @param [String] token
40
+ # @return [String] paypal checkout url to redirect user to
41
+ def self.GetCheckoutURL(token)
42
+ EZPaypal::Config.EndPoint["express_checkout_url"]+ "&token=" + token unless token.nil?
43
+ end
44
+
45
+ # Get current checkout details, only works if token has obtained
46
+ # @param [String] token
47
+ # @return [Hash] checkout details associated with the given token
48
+ def self.GetCheckoutDetails(token)
49
+ unless (token.nil?)
50
+ begin
51
+ # Setup default options
52
+ setting = EZPaypal::Config.Setting.clone
53
+ setting.merge!({"METHOD" => "GetExpressCheckoutDetails"})
54
+ setting.merge!({"TOKEN" => token})
55
+
56
+ # Get checkout details
57
+ query_string = EZPaypal::Helper.ConvertHashToQueryString(setting)
58
+ checkout_details_origin = EZHttp.Send(EZPaypal::Config.EndPoint["express_checkout_endpoint"], query_string, "post").body
59
+
60
+ # Convert checkout details to Hash to return
61
+ checkout_details = EZPaypal::Helper.ConvertParamToHash(checkout_details_origin)
62
+
63
+ return checkout_details
64
+ rescue
65
+ throw "Error occured in GetExpressCheckoutDetails: token=#{token}"
66
+ end
67
+ end
68
+ end
69
+
70
+ # Confirm payment, only works if token and payer_id is obtained
71
+ # @param [String] token
72
+ # @param [String] payer_id
73
+ # @return [Hash] payment details associated with the given token and payer_id
74
+ def self.ConfirmPurchase(token, payer_id)
75
+ unless (token.nil? && payer_id.nil?)
76
+ begin
77
+ # Setup default options
78
+ setting = EZPaypal::Config.Setting.clone
79
+ setting.merge!({"METHOD" => "DoExpressCheckoutPayment"})
80
+ setting.merge!({"PAYMENTREQUEST_0_PAYMENTACTION" => "Sale"})
81
+ setting.merge!({"TOKEN" => token})
82
+ setting.merge!({"PAYERID" => payer_id})
83
+
84
+
85
+ # Get checkout details
86
+ checkout_details_origin = EZPaypal::Request.GetCheckoutDetails(token)
87
+ checkout_details_origin.each do |key, value|
88
+ key = CGI::unescape(key)
89
+ value = CGI::unescape(value)
90
+ end
91
+
92
+ # Submit checkout request to confirm the purchase
93
+ payment_response_origin = HashWithIndifferentAccess.new()
94
+ if (checkout_details_origin["ACK"].downcase == "success")
95
+ checkout_details_origin.merge!(setting)
96
+ query_string = EZPaypal::Helper.ConvertHashToQueryString(checkout_details_origin)
97
+ payment_response_origin = EZHttp.Send(EZPaypal::Config.EndPoint["express_checkout_endpoint"], query_string, "post").body
98
+ end
99
+
100
+ # Convert response to Hash to return
101
+ payment_response = EZPaypal::Helper.ConvertParamToHash(payment_response_origin)
102
+
103
+ return payment_response
104
+
105
+ rescue
106
+ throw "Error occured in ConfirmPurchase: TOKEN=#{token}, PAYERID=#{payer_id}"
107
+ end
108
+ end
109
+ end
110
+
111
+ # Create a recurring profile for a customer
112
+ # @param [EZPaypal::Cart::RecurringProfile] profile including all the config for the recurring profile
113
+ # @return [Hash] profile creation confirmation from paypal
114
+ def self.CreateRecurringProfile(profile)
115
+ begin
116
+ # Setup default options
117
+ setting = EZPaypal::Config.Setting.clone
118
+ default = {"METHOD" => "CreateRecurringPaymentsProfile"}
119
+ setting.merge!(default).merge!(profile)
120
+
121
+ # Http call to create profile and get response
122
+ query_string = EZPaypal::Helper.ConvertHashToQueryString(setting)
123
+ profile_response_origin = EZHttp.Send(EZPaypal::Config.EndPoint["express_checkout_endpoint"], query_string, "post").body
124
+
125
+ # Convert response to Hash to return
126
+ profile_response = EZPaypal::Helper.ConvertParamToHash(profile_response_origin)
127
+
128
+ return profile_response
129
+ rescue
130
+ throw "Error occured in CreateRecurringProfile"
131
+ end
132
+ end
133
+
134
+ # Refund money to a transaction
135
+ # @param [String] all
136
+ # @return [Hash]
137
+ def self.Refund(transaction_id, refund_type, amount, currency, note)
138
+ # Setup default options
139
+ setting = EZPaypal::Config.Setting.clone
140
+ options = {
141
+ "METHOD" => "RefundTransaction",
142
+ "TRANSACTIONID" => transaction_id,
143
+ "REFUNDTYPE" => refund_type || "Partial",
144
+ "AMT" => amount || "0",
145
+ "CURRENCYCODE" => currency || "USD",
146
+ "NOTE" => note || ""
147
+ }
148
+ setting.merge!(options)
149
+
150
+ # Http call to create profile and get response
151
+ query_string = EZPaypal::Helper.ConvertHashToQueryString(setting)
152
+ refund_response_origin = EZHttp.Send(EZPaypal::Config.EndPoint["express_checkout_endpoint"], query_string, "post").body
153
+
154
+ # Convert response to Hash to return
155
+ profile_response = EZPaypal::Helper.ConvertParamToHash(refund_response_origin)
156
+ end
157
+
158
+ end
159
+ end