ez_paypal 1.0.0

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.
@@ -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