aktivemerchant 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (193) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +1596 -0
  3. data/CONTRIBUTORS +511 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +18 -0
  6. data/lib/active_merchant.rb +108 -0
  7. data/lib/active_merchant/billing.rb +13 -0
  8. data/lib/active_merchant/billing/apple_pay_payment_token.rb +22 -0
  9. data/lib/active_merchant/billing/avs_result.rb +98 -0
  10. data/lib/active_merchant/billing/base.rb +72 -0
  11. data/lib/active_merchant/billing/check.rb +76 -0
  12. data/lib/active_merchant/billing/compatibility.rb +120 -0
  13. data/lib/active_merchant/billing/credit_card.rb +352 -0
  14. data/lib/active_merchant/billing/credit_card_formatting.rb +24 -0
  15. data/lib/active_merchant/billing/credit_card_methods.rb +160 -0
  16. data/lib/active_merchant/billing/cvv_result.rb +38 -0
  17. data/lib/active_merchant/billing/gateway.rb +268 -0
  18. data/lib/active_merchant/billing/gateways.rb +17 -0
  19. data/lib/active_merchant/billing/gateways/adyen.rb +209 -0
  20. data/lib/active_merchant/billing/gateways/alfabank.rb +117 -0
  21. data/lib/active_merchant/billing/gateways/app55.rb +176 -0
  22. data/lib/active_merchant/billing/gateways/authorize_net.rb +419 -0
  23. data/lib/active_merchant/billing/gateways/authorize_net_arb.rb +417 -0
  24. data/lib/active_merchant/billing/gateways/authorize_net_cim.rb +976 -0
  25. data/lib/active_merchant/billing/gateways/balanced.rb +256 -0
  26. data/lib/active_merchant/billing/gateways/bank_frick.rb +225 -0
  27. data/lib/active_merchant/billing/gateways/banwire.rb +105 -0
  28. data/lib/active_merchant/billing/gateways/barclays_epdq.rb +314 -0
  29. data/lib/active_merchant/billing/gateways/barclays_epdq_extra_plus.rb +15 -0
  30. data/lib/active_merchant/billing/gateways/be2bill.rb +131 -0
  31. data/lib/active_merchant/billing/gateways/beanstream.rb +188 -0
  32. data/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb +393 -0
  33. data/lib/active_merchant/billing/gateways/beanstream_interac.rb +54 -0
  34. data/lib/active_merchant/billing/gateways/blue_pay.rb +506 -0
  35. data/lib/active_merchant/billing/gateways/bogus.rb +140 -0
  36. data/lib/active_merchant/billing/gateways/borgun.rb +210 -0
  37. data/lib/active_merchant/billing/gateways/braintree.rb +19 -0
  38. data/lib/active_merchant/billing/gateways/braintree/braintree_common.rb +9 -0
  39. data/lib/active_merchant/billing/gateways/braintree_blue.rb +515 -0
  40. data/lib/active_merchant/billing/gateways/braintree_orange.rb +20 -0
  41. data/lib/active_merchant/billing/gateways/bridge_pay.rb +189 -0
  42. data/lib/active_merchant/billing/gateways/card_save.rb +23 -0
  43. data/lib/active_merchant/billing/gateways/card_stream.rb +220 -0
  44. data/lib/active_merchant/billing/gateways/cashnet.rb +191 -0
  45. data/lib/active_merchant/billing/gateways/cc5.rb +201 -0
  46. data/lib/active_merchant/billing/gateways/cecabank.rb +229 -0
  47. data/lib/active_merchant/billing/gateways/certo_direct.rb +278 -0
  48. data/lib/active_merchant/billing/gateways/checkout.rb +213 -0
  49. data/lib/active_merchant/billing/gateways/commercegate.rb +143 -0
  50. data/lib/active_merchant/billing/gateways/conekta.rb +209 -0
  51. data/lib/active_merchant/billing/gateways/cyber_source.rb +709 -0
  52. data/lib/active_merchant/billing/gateways/data_cash.rb +600 -0
  53. data/lib/active_merchant/billing/gateways/efsnet.rb +219 -0
  54. data/lib/active_merchant/billing/gateways/elavon.rb +348 -0
  55. data/lib/active_merchant/billing/gateways/epay.rb +275 -0
  56. data/lib/active_merchant/billing/gateways/evo_ca.rb +308 -0
  57. data/lib/active_merchant/billing/gateways/eway.rb +214 -0
  58. data/lib/active_merchant/billing/gateways/eway_managed.rb +291 -0
  59. data/lib/active_merchant/billing/gateways/eway_rapid.rb +524 -0
  60. data/lib/active_merchant/billing/gateways/exact.rb +218 -0
  61. data/lib/active_merchant/billing/gateways/fat_zebra.rb +173 -0
  62. data/lib/active_merchant/billing/gateways/federated_canada.rb +160 -0
  63. data/lib/active_merchant/billing/gateways/finansbank.rb +23 -0
  64. data/lib/active_merchant/billing/gateways/first_giving.rb +143 -0
  65. data/lib/active_merchant/billing/gateways/first_pay.rb +160 -0
  66. data/lib/active_merchant/billing/gateways/firstdata_e4.rb +355 -0
  67. data/lib/active_merchant/billing/gateways/garanti.rb +257 -0
  68. data/lib/active_merchant/billing/gateways/global_transport.rb +183 -0
  69. data/lib/active_merchant/billing/gateways/hdfc.rb +207 -0
  70. data/lib/active_merchant/billing/gateways/hps.rb +288 -0
  71. data/lib/active_merchant/billing/gateways/iats_payments.rb +251 -0
  72. data/lib/active_merchant/billing/gateways/ideal/ideal_base.rb +246 -0
  73. data/lib/active_merchant/billing/gateways/ideal/ideal_rabobank.pem +13 -0
  74. data/lib/active_merchant/billing/gateways/ideal/ideal_response.rb +29 -0
  75. data/lib/active_merchant/billing/gateways/ideal_rabobank.rb +66 -0
  76. data/lib/active_merchant/billing/gateways/inspire.rb +213 -0
  77. data/lib/active_merchant/billing/gateways/instapay.rb +163 -0
  78. data/lib/active_merchant/billing/gateways/iridium.rb +457 -0
  79. data/lib/active_merchant/billing/gateways/itransact.rb +448 -0
  80. data/lib/active_merchant/billing/gateways/jetpay.rb +275 -0
  81. data/lib/active_merchant/billing/gateways/linkpoint.rb +438 -0
  82. data/lib/active_merchant/billing/gateways/litle.rb +346 -0
  83. data/lib/active_merchant/billing/gateways/maxipago.rb +197 -0
  84. data/lib/active_merchant/billing/gateways/merchant_e_solutions.rb +170 -0
  85. data/lib/active_merchant/billing/gateways/merchant_one.rb +114 -0
  86. data/lib/active_merchant/billing/gateways/merchant_ware.rb +319 -0
  87. data/lib/active_merchant/billing/gateways/merchant_ware_version_four.rb +268 -0
  88. data/lib/active_merchant/billing/gateways/merchant_warrior.rb +195 -0
  89. data/lib/active_merchant/billing/gateways/mercury.rb +333 -0
  90. data/lib/active_merchant/billing/gateways/metrics_global.rb +303 -0
  91. data/lib/active_merchant/billing/gateways/migs.rb +265 -0
  92. data/lib/active_merchant/billing/gateways/migs/migs_codes.rb +100 -0
  93. data/lib/active_merchant/billing/gateways/modern_payments.rb +37 -0
  94. data/lib/active_merchant/billing/gateways/modern_payments_cim.rb +219 -0
  95. data/lib/active_merchant/billing/gateways/moneris.rb +309 -0
  96. data/lib/active_merchant/billing/gateways/moneris_us.rb +291 -0
  97. data/lib/active_merchant/billing/gateways/money_movers.rb +152 -0
  98. data/lib/active_merchant/billing/gateways/nab_transact.rb +280 -0
  99. data/lib/active_merchant/billing/gateways/net_registry.rb +198 -0
  100. data/lib/active_merchant/billing/gateways/netaxept.rb +181 -0
  101. data/lib/active_merchant/billing/gateways/netbilling.rb +190 -0
  102. data/lib/active_merchant/billing/gateways/netpay.rb +223 -0
  103. data/lib/active_merchant/billing/gateways/network_merchants.rb +242 -0
  104. data/lib/active_merchant/billing/gateways/nmi.rb +256 -0
  105. data/lib/active_merchant/billing/gateways/ogone.rb +435 -0
  106. data/lib/active_merchant/billing/gateways/openpay.rb +194 -0
  107. data/lib/active_merchant/billing/gateways/optimal_payment.rb +313 -0
  108. data/lib/active_merchant/billing/gateways/orbital.rb +803 -0
  109. data/lib/active_merchant/billing/gateways/orbital/orbital_soft_descriptors.rb +47 -0
  110. data/lib/active_merchant/billing/gateways/pac_net_raven.rb +207 -0
  111. data/lib/active_merchant/billing/gateways/pago_facil.rb +122 -0
  112. data/lib/active_merchant/billing/gateways/pay_gate_xml.rb +261 -0
  113. data/lib/active_merchant/billing/gateways/pay_junction.rb +390 -0
  114. data/lib/active_merchant/billing/gateways/pay_secure.rb +112 -0
  115. data/lib/active_merchant/billing/gateways/pay_u_latam.rb +462 -0
  116. data/lib/active_merchant/billing/gateways/paybox_direct.rb +188 -0
  117. data/lib/active_merchant/billing/gateways/payex.rb +412 -0
  118. data/lib/active_merchant/billing/gateways/payflow.rb +304 -0
  119. data/lib/active_merchant/billing/gateways/payflow/payflow_common_api.rb +209 -0
  120. data/lib/active_merchant/billing/gateways/payflow/payflow_express_response.rb +39 -0
  121. data/lib/active_merchant/billing/gateways/payflow/payflow_response.rb +13 -0
  122. data/lib/active_merchant/billing/gateways/payflow_express.rb +224 -0
  123. data/lib/active_merchant/billing/gateways/payflow_express_uk.rb +15 -0
  124. data/lib/active_merchant/billing/gateways/payflow_uk.rb +21 -0
  125. data/lib/active_merchant/billing/gateways/payment_express.rb +353 -0
  126. data/lib/active_merchant/billing/gateways/paymill.rb +281 -0
  127. data/lib/active_merchant/billing/gateways/paypal.rb +117 -0
  128. data/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb +670 -0
  129. data/lib/active_merchant/billing/gateways/paypal/paypal_express_response.rb +65 -0
  130. data/lib/active_merchant/billing/gateways/paypal/paypal_recurring_api.rb +262 -0
  131. data/lib/active_merchant/billing/gateways/paypal_ca.rb +13 -0
  132. data/lib/active_merchant/billing/gateways/paypal_digital_goods.rb +44 -0
  133. data/lib/active_merchant/billing/gateways/paypal_express.rb +264 -0
  134. data/lib/active_merchant/billing/gateways/paypal_express_common.rb +30 -0
  135. data/lib/active_merchant/billing/gateways/payscout.rb +162 -0
  136. data/lib/active_merchant/billing/gateways/paystation.rb +199 -0
  137. data/lib/active_merchant/billing/gateways/payway.rb +207 -0
  138. data/lib/active_merchant/billing/gateways/pin.rb +197 -0
  139. data/lib/active_merchant/billing/gateways/plugnpay.rb +283 -0
  140. data/lib/active_merchant/billing/gateways/psigate.rb +216 -0
  141. data/lib/active_merchant/billing/gateways/psl_card.rb +303 -0
  142. data/lib/active_merchant/billing/gateways/qbms.rb +292 -0
  143. data/lib/active_merchant/billing/gateways/quantum.rb +276 -0
  144. data/lib/active_merchant/billing/gateways/quickpay.rb +367 -0
  145. data/lib/active_merchant/billing/gateways/realex.rb +298 -0
  146. data/lib/active_merchant/billing/gateways/redsys.rb +391 -0
  147. data/lib/active_merchant/billing/gateways/sage.rb +175 -0
  148. data/lib/active_merchant/billing/gateways/sage/sage_bankcard.rb +87 -0
  149. data/lib/active_merchant/billing/gateways/sage/sage_core.rb +114 -0
  150. data/lib/active_merchant/billing/gateways/sage/sage_vault.rb +149 -0
  151. data/lib/active_merchant/billing/gateways/sage/sage_virtual_check.rb +102 -0
  152. data/lib/active_merchant/billing/gateways/sage_pay.rb +398 -0
  153. data/lib/active_merchant/billing/gateways/sallie_mae.rb +143 -0
  154. data/lib/active_merchant/billing/gateways/secure_net.rb +252 -0
  155. data/lib/active_merchant/billing/gateways/secure_pay.rb +201 -0
  156. data/lib/active_merchant/billing/gateways/secure_pay_au.rb +281 -0
  157. data/lib/active_merchant/billing/gateways/secure_pay_tech.rb +105 -0
  158. data/lib/active_merchant/billing/gateways/skip_jack.rb +452 -0
  159. data/lib/active_merchant/billing/gateways/smart_ps.rb +283 -0
  160. data/lib/active_merchant/billing/gateways/so_easy_pay.rb +194 -0
  161. data/lib/active_merchant/billing/gateways/spreedly_core.rb +247 -0
  162. data/lib/active_merchant/billing/gateways/stripe.rb +411 -0
  163. data/lib/active_merchant/billing/gateways/swipe_checkout.rb +157 -0
  164. data/lib/active_merchant/billing/gateways/tns.rb +227 -0
  165. data/lib/active_merchant/billing/gateways/trans_first.rb +126 -0
  166. data/lib/active_merchant/billing/gateways/transax.rb +23 -0
  167. data/lib/active_merchant/billing/gateways/transnational.rb +10 -0
  168. data/lib/active_merchant/billing/gateways/trust_commerce.rb +416 -0
  169. data/lib/active_merchant/billing/gateways/usa_epay.rb +25 -0
  170. data/lib/active_merchant/billing/gateways/usa_epay_advanced.rb +1516 -0
  171. data/lib/active_merchant/billing/gateways/usa_epay_transaction.rb +254 -0
  172. data/lib/active_merchant/billing/gateways/verifi.rb +225 -0
  173. data/lib/active_merchant/billing/gateways/viaklix.rb +183 -0
  174. data/lib/active_merchant/billing/gateways/vindicia.rb +385 -0
  175. data/lib/active_merchant/billing/gateways/webpay.rb +97 -0
  176. data/lib/active_merchant/billing/gateways/wepay.rb +189 -0
  177. data/lib/active_merchant/billing/gateways/wirecard.rb +421 -0
  178. data/lib/active_merchant/billing/gateways/worldpay.rb +331 -0
  179. data/lib/active_merchant/billing/gateways/worldpay_us.rb +181 -0
  180. data/lib/active_merchant/billing/model.rb +30 -0
  181. data/lib/active_merchant/billing/payment_token.rb +21 -0
  182. data/lib/active_merchant/billing/rails.rb +3 -0
  183. data/lib/active_merchant/billing/response.rb +91 -0
  184. data/lib/active_merchant/country.rb +332 -0
  185. data/lib/active_merchant/empty.rb +20 -0
  186. data/lib/active_merchant/errors.rb +29 -0
  187. data/lib/active_merchant/offsite_payments_shim.rb +19 -0
  188. data/lib/active_merchant/version.rb +3 -0
  189. data/lib/activemerchant.rb +1 -0
  190. data/lib/support/gateway_support.rb +71 -0
  191. data/lib/support/outbound_hosts.rb +25 -0
  192. data/lib/support/ssl_verify.rb +93 -0
  193. metadata +400 -0
@@ -0,0 +1,281 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ class PaymillGateway < Gateway
4
+ self.supported_countries = %w(AD AT BE BG CH CY CZ DE DK EE ES FI FO FR GB
5
+ GI GR HR HU IE IL IM IS IT LI LT LU LV MC MT
6
+ NL NO PL PT RO SE SI SK TR VA)
7
+
8
+ self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :discover, :union_pay, :jcb]
9
+ self.homepage_url = 'https://paymill.com'
10
+ self.display_name = 'PAYMILL'
11
+ self.money_format = :cents
12
+ self.default_currency = 'EUR'
13
+
14
+ def initialize(options = {})
15
+ requires!(options, :public_key, :private_key)
16
+ super
17
+ end
18
+
19
+ def purchase(money, payment_method, options = {})
20
+ action_with_token(:purchase, money, payment_method, options)
21
+ end
22
+
23
+ def authorize(money, payment_method, options = {})
24
+ action_with_token(:authorize, money, payment_method, options)
25
+ end
26
+
27
+ def capture(money, authorization, options = {})
28
+ post = {}
29
+
30
+ add_amount(post, money, options)
31
+ post[:preauthorization] = preauth(authorization)
32
+ post[:description] = options[:description]
33
+ post[:source] = 'active_merchant'
34
+ commit(:post, 'transactions', post)
35
+ end
36
+
37
+ def refund(money, authorization, options={})
38
+ post = {}
39
+
40
+ post[:amount] = amount(money)
41
+ post[:description] = options[:description]
42
+ commit(:post, "refunds/#{transaction_id(authorization)}", post)
43
+ end
44
+
45
+ def void(authorization, options={})
46
+ commit(:delete, "preauthorizations/#{preauth(authorization)}")
47
+ end
48
+
49
+ def store(credit_card, options={})
50
+ save_card(credit_card)
51
+ end
52
+
53
+ private
54
+
55
+ def add_credit_card(post, credit_card)
56
+ post['account.number'] = credit_card.number
57
+ post['account.expiry.month'] = sprintf("%.2i", credit_card.month)
58
+ post['account.expiry.year'] = sprintf("%.4i", credit_card.year)
59
+ post['account.verification'] = credit_card.verification_value
60
+ end
61
+
62
+ def headers
63
+ { 'Authorization' => ('Basic ' + Base64.strict_encode64("#{@options[:private_key]}:X").chomp) }
64
+ end
65
+
66
+ def commit(method, url, parameters=nil)
67
+ begin
68
+ raw_response = ssl_request(method, "https://api.paymill.com/v2/#{url}", post_data(parameters), headers)
69
+ rescue ResponseError => e
70
+ begin
71
+ parsed = JSON.parse(e.response.body)
72
+ rescue JSON::ParserError
73
+ return Response.new(false, "Unable to parse error response: '#{e.response.body}'")
74
+ end
75
+ return Response.new(false, response_message(parsed), parsed, {})
76
+ end
77
+
78
+ response_from(raw_response)
79
+ end
80
+
81
+ def response_from(raw_response)
82
+ parsed = JSON.parse(raw_response)
83
+ options = {
84
+ :authorization => authorization_from(parsed),
85
+ :test => (parsed['mode'] == 'test'),
86
+ }
87
+
88
+ succeeded = (parsed['data'] == []) || (parsed['data']['response_code'] == 20000)
89
+ Response.new(succeeded, response_message(parsed), parsed, options)
90
+ end
91
+
92
+ def authorization_from(parsed_response)
93
+ parsed_data = parsed_response['data']
94
+ return '' unless parsed_data.kind_of?(Hash)
95
+
96
+ [
97
+ parsed_data['id'],
98
+ parsed_data['preauthorization'].try(:[], 'id')
99
+ ].join(";")
100
+ end
101
+
102
+ def action_with_token(action, money, payment_method, options)
103
+ case payment_method
104
+ when String
105
+ self.send("#{action}_with_token", money, payment_method, options)
106
+ else
107
+ MultiResponse.run do |r|
108
+ r.process { save_card(payment_method) }
109
+ r.process { self.send("#{action}_with_token", money, r.authorization, options) }
110
+ end
111
+ end
112
+ end
113
+
114
+ def purchase_with_token(money, card_token, options)
115
+ post = {}
116
+
117
+ add_amount(post, money, options)
118
+ post[:token] = card_token
119
+ post[:description] = options[:description]
120
+ post[:source] = 'active_merchant'
121
+ commit(:post, 'transactions', post)
122
+ end
123
+
124
+ def authorize_with_token(money, card_token, options)
125
+ post = {}
126
+
127
+ add_amount(post, money, options)
128
+ post[:token] = card_token
129
+ post[:description] = options[:description]
130
+ post[:source] = 'active_merchant'
131
+ commit(:post, 'preauthorizations', post)
132
+ end
133
+
134
+ def save_card(credit_card)
135
+ post = {}
136
+
137
+ add_credit_card(post, credit_card)
138
+ post['channel.id'] = @options[:public_key]
139
+ post['jsonPFunction'] = 'jsonPFunction'
140
+ post['transaction.mode'] = (test? ? 'CONNECTOR_TEST' : 'LIVE')
141
+
142
+ begin
143
+ raw_response = ssl_request(:get, "#{save_card_url}?#{post_data(post)}", nil, {})
144
+ rescue ResponseError => e
145
+ return Response.new(false, e.response.body, e.response.body, {})
146
+ end
147
+
148
+ response_for_save_from(raw_response)
149
+ end
150
+
151
+ def response_for_save_from(raw_response)
152
+ options = { :test => test? }
153
+
154
+ parser = ResponseParser.new(raw_response, options)
155
+ parser.generate_response
156
+ end
157
+
158
+ def parse_reponse(response)
159
+ JSON.parse(response.sub(/jsonPFunction\(/, '').sub(/\)\z/, ''))
160
+ end
161
+
162
+ def save_card_url
163
+ (test? ? 'https://test-token.paymill.com' : 'https://token-v2.paymill.de')
164
+ end
165
+
166
+ def post_data(params)
167
+ return nil unless params
168
+
169
+ no_blanks = params.reject { |key, value| value.blank? }
170
+ no_blanks.map { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&")
171
+ end
172
+
173
+ def add_amount(post, money, options)
174
+ post[:amount] = amount(money)
175
+ post[:currency] = (options[:currency] || currency(money))
176
+ end
177
+
178
+ def preauth(authorization)
179
+ authorization.split(";").last
180
+ end
181
+
182
+ def transaction_id(authorization)
183
+ authorization.split(';').first
184
+ end
185
+
186
+ RESPONSE_CODES = {
187
+ 10001 => "General undefined response.",
188
+ 10002 => "Still waiting on something.",
189
+
190
+ 20000 => "General success response.",
191
+
192
+ 40000 => "General problem with data.",
193
+ 40001 => "General problem with payment data.",
194
+ 40100 => "Problem with credit card data.",
195
+ 40101 => "Problem with cvv.",
196
+ 40102 => "Card expired or not yet valid.",
197
+ 40103 => "Limit exceeded.",
198
+ 40104 => "Card invalid.",
199
+ 40105 => "Expiry date not valid.",
200
+ 40106 => "Credit card brand required.",
201
+ 40200 => "Problem with bank account data.",
202
+ 40201 => "Bank account data combination mismatch.",
203
+ 40202 => "User authentication failed.",
204
+ 40300 => "Problem with 3d secure data.",
205
+ 40301 => "Currency / amount mismatch",
206
+ 40400 => "Problem with input data.",
207
+ 40401 => "Amount too low or zero.",
208
+ 40402 => "Usage field too long.",
209
+ 40403 => "Currency not allowed.",
210
+
211
+ 50000 => "General problem with backend.",
212
+ 50001 => "Country blacklisted.",
213
+ 50100 => "Technical error with credit card.",
214
+ 50101 => "Error limit exceeded.",
215
+ 50102 => "Card declined by authorization system.",
216
+ 50103 => "Manipulation or stolen card.",
217
+ 50104 => "Card restricted.",
218
+ 50105 => "Invalid card configuration data.",
219
+ 50200 => "Technical error with bank account.",
220
+ 50201 => "Card blacklisted.",
221
+ 50300 => "Technical error with 3D secure.",
222
+ 50400 => "Decline because of risk issues.",
223
+ 50500 => "General timeout.",
224
+ 50501 => "Timeout on side of the acquirer.",
225
+ 50502 => "Risk management transaction timeout.",
226
+ 50600 => "Duplicate transaction."
227
+ }
228
+
229
+ def response_message(parsed_response)
230
+ return parsed_response["error"] if parsed_response["error"]
231
+ return "Transaction approved." if (parsed_response['data'] == [])
232
+
233
+ code = parsed_response["data"]["response_code"]
234
+ RESPONSE_CODES[code] || code.to_s
235
+ end
236
+
237
+
238
+ class ResponseParser
239
+ attr_reader :raw_response, :parsed, :succeeded, :message, :options
240
+
241
+ def initialize(raw_response="", options={})
242
+ @raw_response = raw_response
243
+ @options = options
244
+ end
245
+
246
+ def generate_response
247
+ parse_response
248
+ if parsed['error']
249
+ handle_response_parse_error
250
+ else
251
+ handle_response_correct_parsing
252
+ end
253
+
254
+ Response.new(succeeded, message, parsed, options)
255
+ end
256
+
257
+ private
258
+
259
+ def parse_response
260
+ @parsed = JSON.parse(raw_response.sub(/jsonPFunction\(/, '').sub(/\)\z/, ''))
261
+ end
262
+
263
+ def handle_response_parse_error
264
+ @succeeded = false
265
+ @message = parsed['error']['message']
266
+ end
267
+
268
+ def handle_response_correct_parsing
269
+ @message = parsed['transaction']['processing']['return']['message']
270
+ if @succeeded = is_ack?
271
+ @options[:authorization] = parsed['transaction']['identification']['uniqueId']
272
+ end
273
+ end
274
+
275
+ def is_ack?
276
+ parsed['transaction']['processing']['result'] == 'ACK'
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,117 @@
1
+ require File.dirname(__FILE__) + '/paypal/paypal_common_api'
2
+ require File.dirname(__FILE__) + '/paypal/paypal_recurring_api'
3
+ require File.dirname(__FILE__) + '/paypal_express'
4
+
5
+ module ActiveMerchant #:nodoc:
6
+ module Billing #:nodoc:
7
+ class PaypalGateway < Gateway
8
+ include PaypalCommonAPI
9
+ include PaypalRecurringApi
10
+
11
+ self.supported_cardtypes = [:visa, :master, :american_express, :discover]
12
+ self.supported_countries = ['US']
13
+ self.homepage_url = 'https://www.paypal.com/us/webapps/mpp/paypal-payments-pro'
14
+ self.display_name = 'PayPal Payments Pro (US)'
15
+
16
+ def authorize(money, credit_card_or_referenced_id, options = {})
17
+ requires!(options, :ip)
18
+ commit define_transaction_type(credit_card_or_referenced_id), build_sale_or_authorization_request('Authorization', money, credit_card_or_referenced_id, options)
19
+ end
20
+
21
+ def purchase(money, credit_card_or_referenced_id, options = {})
22
+ requires!(options, :ip)
23
+ commit define_transaction_type(credit_card_or_referenced_id), build_sale_or_authorization_request('Sale', money, credit_card_or_referenced_id, options)
24
+ end
25
+
26
+ def verify(credit_card, options = {})
27
+ if %w(visa master).include?(credit_card.brand)
28
+ authorize(0, credit_card, options)
29
+ else
30
+ MultiResponse.run(:use_first_response) do |r|
31
+ r.process { authorize(100, credit_card, options) }
32
+ r.process(:ignore_result) { void(r.authorization, options) }
33
+ end
34
+ end
35
+ end
36
+
37
+ def express
38
+ @express ||= PaypalExpressGateway.new(@options)
39
+ end
40
+
41
+ private
42
+
43
+ def define_transaction_type(transaction_arg)
44
+ if transaction_arg.is_a?(String)
45
+ return 'DoReferenceTransaction'
46
+ else
47
+ return 'DoDirectPayment'
48
+ end
49
+ end
50
+
51
+ def build_sale_or_authorization_request(action, money, credit_card_or_referenced_id, options)
52
+ transaction_type = define_transaction_type(credit_card_or_referenced_id)
53
+ reference_id = credit_card_or_referenced_id if transaction_type == "DoReferenceTransaction"
54
+
55
+ billing_address = options[:billing_address] || options[:address]
56
+ currency_code = options[:currency] || currency(money)
57
+
58
+ xml = Builder::XmlMarkup.new :indent => 2
59
+ xml.tag! transaction_type + 'Req', 'xmlns' => PAYPAL_NAMESPACE do
60
+ xml.tag! transaction_type + 'Request', 'xmlns:n2' => EBAY_NAMESPACE do
61
+ xml.tag! 'n2:Version', API_VERSION
62
+ xml.tag! 'n2:' + transaction_type + 'RequestDetails' do
63
+ xml.tag! 'n2:ReferenceID', reference_id if transaction_type == 'DoReferenceTransaction'
64
+ xml.tag! 'n2:PaymentAction', action
65
+ add_payment_details(xml, money, currency_code, options)
66
+ add_credit_card(xml, credit_card_or_referenced_id, billing_address, options) unless transaction_type == 'DoReferenceTransaction'
67
+ xml.tag! 'n2:IPAddress', options[:ip]
68
+ end
69
+ end
70
+ end
71
+
72
+ xml.target!
73
+ end
74
+
75
+ def add_credit_card(xml, credit_card, address, options)
76
+ xml.tag! 'n2:CreditCard' do
77
+ xml.tag! 'n2:CreditCardType', credit_card_type(card_brand(credit_card))
78
+ xml.tag! 'n2:CreditCardNumber', credit_card.number
79
+ xml.tag! 'n2:ExpMonth', format(credit_card.month, :two_digits)
80
+ xml.tag! 'n2:ExpYear', format(credit_card.year, :four_digits)
81
+ xml.tag! 'n2:CVV2', credit_card.verification_value
82
+
83
+ if [ 'switch', 'solo' ].include?(card_brand(credit_card).to_s)
84
+ xml.tag! 'n2:StartMonth', format(credit_card.start_month, :two_digits) unless credit_card.start_month.blank?
85
+ xml.tag! 'n2:StartYear', format(credit_card.start_year, :four_digits) unless credit_card.start_year.blank?
86
+ xml.tag! 'n2:IssueNumber', format(credit_card.issue_number, :two_digits) unless credit_card.issue_number.blank?
87
+ end
88
+
89
+ xml.tag! 'n2:CardOwner' do
90
+ xml.tag! 'n2:PayerName' do
91
+ xml.tag! 'n2:FirstName', credit_card.first_name
92
+ xml.tag! 'n2:LastName', credit_card.last_name
93
+ end
94
+
95
+ xml.tag! 'n2:Payer', options[:email]
96
+ add_address(xml, 'n2:Address', address)
97
+ end
98
+ end
99
+ end
100
+
101
+ def credit_card_type(type)
102
+ case type
103
+ when 'visa' then 'Visa'
104
+ when 'master' then 'MasterCard'
105
+ when 'discover' then 'Discover'
106
+ when 'american_express' then 'Amex'
107
+ when 'switch' then 'Switch'
108
+ when 'solo' then 'Solo'
109
+ end
110
+ end
111
+
112
+ def build_response(success, message, response, options = {})
113
+ Response.new(success, message, response, options)
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,670 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ # This module is included in both PaypalGateway and PaypalExpressGateway
4
+ module PaypalCommonAPI
5
+ API_VERSION = '72'
6
+
7
+ URLS = {
8
+ :test => { :certificate => 'https://api.sandbox.paypal.com/2.0/',
9
+ :signature => 'https://api-3t.sandbox.paypal.com/2.0/' },
10
+ :live => { :certificate => 'https://api.paypal.com/2.0/',
11
+ :signature => 'https://api-3t.paypal.com/2.0/' }
12
+ }
13
+
14
+ PAYPAL_NAMESPACE = 'urn:ebay:api:PayPalAPI'
15
+ EBAY_NAMESPACE = 'urn:ebay:apis:eBLBaseComponents'
16
+
17
+ ENVELOPE_NAMESPACES = { 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
18
+ 'xmlns:env' => 'http://schemas.xmlsoap.org/soap/envelope/',
19
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
20
+ }
21
+ CREDENTIALS_NAMESPACES = { 'xmlns' => PAYPAL_NAMESPACE,
22
+ 'xmlns:n1' => EBAY_NAMESPACE,
23
+ 'env:mustUnderstand' => '0'
24
+ }
25
+
26
+ AUSTRALIAN_STATES = {
27
+ 'ACT' => 'Australian Capital Territory',
28
+ 'NSW' => 'New South Wales',
29
+ 'NT' => 'Northern Territory',
30
+ 'QLD' => 'Queensland',
31
+ 'SA' => 'South Australia',
32
+ 'TAS' => 'Tasmania',
33
+ 'VIC' => 'Victoria',
34
+ 'WA' => 'Western Australia'
35
+ }
36
+
37
+ SUCCESS_CODES = [ 'Success', 'SuccessWithWarning' ]
38
+
39
+ FRAUD_REVIEW_CODE = "11610"
40
+
41
+ def self.included(base)
42
+ base.default_currency = 'USD'
43
+ base.cattr_accessor :pem_file
44
+ base.cattr_accessor :signature
45
+ base.live_url = URLS[:live][:signature]
46
+ base.test_url = URLS[:test][:signature]
47
+ end
48
+
49
+ # The gateway must be configured with either your PayPal PEM file
50
+ # or your PayPal API Signature. Only one is required.
51
+ #
52
+ # <tt>:pem</tt> The text of your PayPal PEM file. Note
53
+ # this is not the path to file, but its
54
+ # contents. If you are only using one PEM
55
+ # file on your site you can declare it
56
+ # globally and then you won't need to
57
+ # include this option
58
+ #
59
+ # <tt>:signature</tt> The text of your PayPal signature.
60
+ # If you are only using one API Signature
61
+ # on your site you can declare it
62
+ # globally and then you won't need to
63
+ # include this option
64
+ def initialize(options = {})
65
+ requires!(options, :login, :password)
66
+
67
+ headers = {'X-PP-AUTHORIZATION' => options.delete(:auth_signature), 'X-PAYPAL-MESSAGE-PROTOCOL' => 'SOAP11'} if options[:auth_signature]
68
+ options = {
69
+ :pem => pem_file,
70
+ :signature => signature,
71
+ :headers => headers || {}
72
+ }.update(options)
73
+
74
+
75
+ if options[:pem].blank? && options[:signature].blank?
76
+ raise ArgumentError, "An API Certificate or API Signature is required to make requests to PayPal"
77
+ end
78
+
79
+ super(options)
80
+ end
81
+
82
+ def reauthorize(money, authorization, options = {})
83
+ commit 'DoReauthorization', build_reauthorize_request(money, authorization, options)
84
+ end
85
+
86
+ def capture(money, authorization, options = {})
87
+ commit 'DoCapture', build_capture_request(money, authorization, options)
88
+ end
89
+
90
+ # Transfer money to one or more recipients.
91
+ #
92
+ # gateway.transfer 1000, 'bob@example.com',
93
+ # :subject => "The money I owe you", :note => "Sorry it's so late"
94
+ #
95
+ # gateway.transfer [1000, 'fred@example.com'],
96
+ # [2450, 'wilma@example.com', :note => 'You will receive another payment on 3/24'],
97
+ # [2000, 'barney@example.com'],
98
+ # :subject => "Your Earnings", :note => "Thanks for your business."
99
+ #
100
+ def transfer(*args)
101
+ commit 'MassPay', build_mass_pay_request(*args)
102
+ end
103
+
104
+ def void(authorization, options = {})
105
+ commit 'DoVoid', build_void_request(authorization, options)
106
+ end
107
+
108
+ # Refunds a transaction.
109
+ #
110
+ # For a full refund pass nil for the amount:
111
+ #
112
+ # gateway.refund nil, 'G39883289DH238'
113
+ #
114
+ # This will automatically make the :refund_type be "Full".
115
+ #
116
+ # For a partial refund just pass the amount as usual:
117
+ #
118
+ # gateway.refund 100, 'UBU83983N920'
119
+ #
120
+ def refund(money, identification, options = {})
121
+ commit 'RefundTransaction', build_refund_request(money, identification, options)
122
+ end
123
+
124
+ def credit(money, identification, options = {})
125
+ ActiveMerchant.deprecated Gateway::CREDIT_DEPRECATION_MESSAGE
126
+ refund(money, identification, options)
127
+ end
128
+
129
+ # ==== For full documentation see {Paypal API Reference:}[https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_soap_r_DoReferenceTransaction]
130
+ # ==== Parameter:
131
+ # * <tt>:money</tt> -- (Required) The amount of this new transaction,
132
+ # required fo the payment details portion of this request
133
+ #
134
+ # ==== Options:
135
+ # * <tt>:reference_id</tt> -- (Required) A transaction ID from a previous purchase, such as a credit card charge using the DoDirectPayment API, or a billing agreement ID.
136
+ # * <tt>:payment_action</tt> -- (Optional) How you want to obtain payment. It is one of the following values:
137
+ #
138
+ # Authorization – This payment is a basic authorization subject to settlement with PayPal Authorization and Capture.
139
+ # Sale – This is a final sale for which you are requesting payment.
140
+ #
141
+ # * <tt>:ip_address</tt> -- (Optional) IP address of the buyer’s browser.
142
+ # Note: PayPal records this IP addresses as a means to detect possible fraud.
143
+ # * <tt>:req_confirm_shipping</tt> -- Whether you require that the buyer’s shipping address on file with PayPal be a confirmed address. You must have permission from PayPal to not require a confirmed address. It is one of the following values:
144
+ #
145
+ # 0 – You do not require that the buyer’s shipping address be a confirmed address.
146
+ # 1 – You require that the buyer’s shipping address be a confirmed address.
147
+ #
148
+ # * <tt>:merchant_session_id</tt> -- (Optional) Your buyer session identification token.
149
+ # * <tt>:return_fmf_details</tt> -- (Optional) Flag to indicate whether you want the results returned by Fraud Management Filters. By default, you do not receive this information. It is one of the following values:
150
+ #
151
+ # 0 – Do not receive FMF details (default)
152
+ # 1 – Receive FMF details
153
+ #
154
+ # * <tt>:soft_descriptor</tt> -- (Optional) Per transaction description of the payment that is passed to the consumer’s credit card statement. If the API request provides a value for the soft descriptor field, the full descriptor displayed on the buyer’s statement has the following format:
155
+ #
156
+ # <PP * | PAYPAL *><Merchant descriptor as set in the Payment Receiving Preferences><1 space><soft descriptor>
157
+ # The soft descriptor can contain only the following characters:
158
+ #
159
+ # Alphanumeric characters
160
+ # - (dash)
161
+ # * (asterisk)
162
+ # . (period)
163
+ # {space}
164
+ #
165
+ def reference_transaction(money, options = {})
166
+ requires!(options, :reference_id)
167
+ commit 'DoReferenceTransaction', build_reference_transaction_request(money, options)
168
+ end
169
+
170
+ def transaction_details(transaction_id)
171
+ commit 'GetTransactionDetails', build_get_transaction_details(transaction_id)
172
+ end
173
+
174
+ # ==== For full documentation see {PayPal API Reference}[https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_soap_r_TransactionSearch]
175
+ # ==== Options:
176
+ # * <tt>:payer </tt> -- (Optional) Search by the buyer’s email address.
177
+ # * <tt>:receipt_id </tt> -- (Optional) Search by the PayPal Account Optional receipt ID.
178
+ # * <tt>:receiver </tt> -- (Optional) Search by the receiver’s email address. If the merchant account has only one email address, this is the primary email. It can also be a non-primary email address.
179
+ # * <tt>:transaction_id</tt> -- (Optional) Search by the transaction ID. The returned results are from the merchant’s transaction records.
180
+ # * <tt>:invoice_id</tt> -- (Optional) Search by invoice identification key, as set by you for the original transaction. This field searches the records for items the merchant sells, not the items purchased.
181
+ # * <tt>:card_number </tt> -- (Optional) Search by credit card number, as set by you for the original transaction. This field searches the records for items the merchant sells, not the items purchased.
182
+ # * <tt>:auction_item_number </tt> -- (Optional) Search by auction item number of the purchased goods.
183
+ # * <tt>:transaction_class </tt> -- (Optional) Search by classification of transaction. Some kinds of possible classes of transactions are not searchable with this field. You cannot search for bank transfer withdrawals, for example. It is one of the following values:
184
+ # All – All transaction classifications
185
+ # Sent – Only payments sent
186
+ # Received – Only payments received
187
+ # MassPay – Only mass payments
188
+ # MoneyRequest – Only money requests
189
+ # FundsAdded – Only funds added to balance
190
+ # FundsWithdrawn – Only funds withdrawn from balance
191
+ # Referral – Only transactions involving referrals
192
+ # Fee – Only transactions involving fees
193
+ # Subscription – Only transactions involving subscriptions
194
+ # Dividend – Only transactions involving dividends
195
+ # Billpay – Only transactions involving BillPay Transactions
196
+ # Refund – Only transactions involving funds
197
+ # CurrencyConversions – Only transactions involving currency conversions
198
+ # BalanceTransfer – Only transactions involving balance transfers
199
+ # Reversal – Only transactions involving BillPay reversals
200
+ # Shipping – Only transactions involving UPS shipping fees
201
+ # BalanceAffecting – Only transactions that affect the account balance
202
+ # ECheck – Only transactions involving eCheck
203
+ #
204
+ # * <tt>:currency_code </tt> -- (Optional) Search by currency code.
205
+ # * <tt>:status</tt> -- (Optional) Search by transaction status. It is one of the following values:
206
+ # One of:
207
+ # Pending – The payment is pending. The specific reason the payment is pending is returned by the GetTransactionDetails API PendingReason field.
208
+ # Processing – The payment is being processed.
209
+ # Success – The payment has been completed and the funds have been added successfully to your account balance.
210
+ # Denied – You denied the payment. This happens only if the payment was previously pending.
211
+ # Reversed – A payment was reversed due to a chargeback or other type of reversal. The funds have been removed from your account balance and returned to the buyer.
212
+ #
213
+ def transaction_search(options)
214
+ requires!(options, :start_date)
215
+ commit 'TransactionSearch', build_transaction_search(options)
216
+ end
217
+
218
+ # ==== Parameters:
219
+ # * <tt>:return_all_currencies</tt> -- Either '1' or '0'
220
+ # 0 – Return only the balance for the primary currency holding.
221
+ # 1 – Return the balance for each currency holding.
222
+ #
223
+ def balance(return_all_currencies = false)
224
+ clean_currency_argument = case return_all_currencies
225
+ when 1, '1' , true; '1'
226
+ else
227
+ '0'
228
+ end
229
+ commit 'GetBalance', build_get_balance(clean_currency_argument)
230
+ end
231
+
232
+ # DoAuthorization takes the transaction_id returned when you call
233
+ # DoExpressCheckoutPayment with a PaymentAction of 'Order'.
234
+ # When you did that, you created an order authorization subject to settlement
235
+ # with PayPal DoAuthorization and DoCapture
236
+ #
237
+ # ==== Parameters:
238
+ # * <tt>:transaction_id</tt> -- The ID returned by DoExpressCheckoutPayment with a PaymentAction of 'Order'.
239
+ # * <tt>:money</tt> -- The amount of money to be authorized for this purchase.
240
+ #
241
+ def authorize_transaction(transaction_id, money, options = {})
242
+ commit 'DoAuthorization', build_do_authorize(transaction_id, money, options)
243
+ end
244
+
245
+ # The ManagePendingTransactionStatus API operation accepts or denies a
246
+ # pending transaction held by Fraud Management Filters.
247
+ #
248
+ # ==== Parameters:
249
+ # * <tt>:transaction_id</tt> -- The ID of the transaction held by Fraud Management Filters.
250
+ # * <tt>:action</tt> -- Either 'Accept' or 'Deny'
251
+ #
252
+ def manage_pending_transaction(transaction_id, action)
253
+ commit 'ManagePendingTransactionStatus', build_manage_pending_transaction_status(transaction_id, action)
254
+ end
255
+
256
+ private
257
+ def build_request_wrapper(action, options = {})
258
+ xml = Builder::XmlMarkup.new :indent => 2
259
+ xml.tag! action + 'Req', 'xmlns' => PAYPAL_NAMESPACE do
260
+ xml.tag! action + 'Request', 'xmlns:n2' => EBAY_NAMESPACE do
261
+ xml.tag! 'n2:Version', API_VERSION
262
+ if options[:request_details]
263
+ xml.tag! 'n2:' + action + 'RequestDetails' do
264
+ yield(xml)
265
+ end
266
+ else
267
+ yield(xml)
268
+ end
269
+ end
270
+ end
271
+ xml.target!
272
+ end
273
+
274
+ def build_do_authorize(transaction_id, money, options = {})
275
+ build_request_wrapper('DoAuthorization') do |xml|
276
+ xml.tag! 'TransactionID', transaction_id
277
+ xml.tag! 'Amount', amount(money), 'currencyID' => options[:currency] || currency(money)
278
+ end
279
+ end
280
+
281
+ def build_reauthorize_request(money, authorization, options)
282
+ xml = Builder::XmlMarkup.new
283
+
284
+ xml.tag! 'DoReauthorizationReq', 'xmlns' => PAYPAL_NAMESPACE do
285
+ xml.tag! 'DoReauthorizationRequest', 'xmlns:n2' => EBAY_NAMESPACE do
286
+ xml.tag! 'n2:Version', API_VERSION
287
+ xml.tag! 'AuthorizationID', authorization
288
+ xml.tag! 'Amount', amount(money), 'currencyID' => options[:currency] || currency(money)
289
+ end
290
+ end
291
+
292
+ xml.target!
293
+ end
294
+
295
+ def build_capture_request(money, authorization, options)
296
+ xml = Builder::XmlMarkup.new
297
+
298
+ xml.tag! 'DoCaptureReq', 'xmlns' => PAYPAL_NAMESPACE do
299
+ xml.tag! 'DoCaptureRequest', 'xmlns:n2' => EBAY_NAMESPACE do
300
+ xml.tag! 'n2:Version', API_VERSION
301
+ xml.tag! 'AuthorizationID', authorization
302
+ xml.tag! 'Amount', amount(money), 'currencyID' => options[:currency] || currency(money)
303
+ xml.tag! 'CompleteType', options[:complete_type] || 'Complete'
304
+ xml.tag! 'InvoiceID', options[:order_id] unless options[:order_id].blank?
305
+ xml.tag! 'Note', options[:description]
306
+ end
307
+ end
308
+
309
+ xml.target!
310
+ end
311
+
312
+ def build_refund_request(money, identification, options)
313
+ xml = Builder::XmlMarkup.new
314
+
315
+ xml.tag! 'RefundTransactionReq', 'xmlns' => PAYPAL_NAMESPACE do
316
+ xml.tag! 'RefundTransactionRequest', 'xmlns:n2' => EBAY_NAMESPACE do
317
+ xml.tag! 'n2:Version', API_VERSION
318
+ xml.tag! 'TransactionID', identification
319
+ xml.tag! 'Amount', amount(money), 'currencyID' => (options[:currency] || currency(money)) if money.present?
320
+ xml.tag! 'RefundType', (options[:refund_type] || (money.present? ? 'Partial' : 'Full'))
321
+ xml.tag! 'Memo', options[:note] unless options[:note].blank?
322
+ end
323
+ end
324
+
325
+ xml.target!
326
+ end
327
+
328
+ def build_void_request(authorization, options)
329
+ xml = Builder::XmlMarkup.new
330
+
331
+ xml.tag! 'DoVoidReq', 'xmlns' => PAYPAL_NAMESPACE do
332
+ xml.tag! 'DoVoidRequest', 'xmlns:n2' => EBAY_NAMESPACE do
333
+ xml.tag! 'n2:Version', API_VERSION
334
+ xml.tag! 'AuthorizationID', authorization
335
+ xml.tag! 'Note', options[:description]
336
+ end
337
+ end
338
+
339
+ xml.target!
340
+ end
341
+
342
+ def build_mass_pay_request(*args)
343
+ default_options = args.last.is_a?(Hash) ? args.pop : {}
344
+ recipients = args.first.is_a?(Array) ? args : [args]
345
+ receiver_type = default_options[:receiver_type]
346
+
347
+ xml = Builder::XmlMarkup.new
348
+
349
+ xml.tag! 'MassPayReq', 'xmlns' => PAYPAL_NAMESPACE do
350
+ xml.tag! 'MassPayRequest', 'xmlns:n2' => EBAY_NAMESPACE do
351
+ xml.tag! 'n2:Version', API_VERSION
352
+ xml.tag! 'EmailSubject', default_options[:subject] if default_options[:subject]
353
+ xml.tag! 'ReceiverType', receiver_type if receiver_type
354
+ recipients.each do |money, recipient, options|
355
+ options ||= default_options
356
+ xml.tag! 'MassPayItem' do
357
+ if(!receiver_type || receiver_type == 'EmailAddress')
358
+ xml.tag! 'ReceiverEmail', recipient
359
+ elsif receiver_type == 'UserID'
360
+ xml.tag! 'ReceiverID', recipient
361
+ else
362
+ raise ArgumentError.new("Unknown receiver_type: #{receiver_type}")
363
+ end
364
+ xml.tag! 'Amount', amount(money), 'currencyID' => (options[:currency] || currency(money))
365
+ xml.tag! 'Note', options[:note] if options[:note]
366
+ xml.tag! 'UniqueId', options[:unique_id] if options[:unique_id]
367
+ end
368
+ end
369
+ end
370
+ end
371
+
372
+ xml.target!
373
+ end
374
+
375
+ def build_manage_pending_transaction_status(transaction_id, action)
376
+ build_request_wrapper('ManagePendingTransactionStatus') do |xml|
377
+ xml.tag! 'TransactionID', transaction_id
378
+ xml.tag! 'Action', action
379
+ end
380
+ end
381
+
382
+ def build_reference_transaction_request(money, options)
383
+ opts = options.dup
384
+ opts[:ip_address] ||= opts[:ip]
385
+ currency_code = opts[:currency] || currency(money)
386
+ reference_transaction_optional_fields = %w{ n2:ReferenceID n2:PaymentAction
387
+ n2:PaymentType n2:IPAddress
388
+ n2:ReqConfirmShipping n2:MerchantSessionId
389
+ n2:ReturnFMFDetails n2:SoftDescriptor }
390
+ build_request_wrapper('DoReferenceTransaction', :request_details => true) do |xml|
391
+ add_optional_fields(xml, reference_transaction_optional_fields, opts)
392
+ add_payment_details(xml, money, currency_code, opts)
393
+ end
394
+ end
395
+
396
+ def build_get_transaction_details(transaction_id)
397
+ build_request_wrapper('GetTransactionDetails') do |xml|
398
+ xml.tag! 'TransactionID', transaction_id
399
+ end
400
+ end
401
+
402
+ def build_transaction_search(options)
403
+ currency_code = options[:currency_code]
404
+ currency_code ||= currency(options[:amount]) if options[:amount]
405
+ transaction_search_optional_fields = %w{ Payer ReceiptID Receiver
406
+ TransactionID InvoiceID CardNumber
407
+ AuctionItemNumber TransactionClass
408
+ CurrencyCode Status ProfileID }
409
+ build_request_wrapper('TransactionSearch') do |xml|
410
+ xml.tag! 'StartDate', date_to_iso(options[:start_date])
411
+ xml.tag! 'EndDate', date_to_iso(options[:end_date]) unless options[:end_date].blank?
412
+ add_optional_fields(xml, transaction_search_optional_fields, options)
413
+ xml.tag! 'Amount', localized_amount(options[:amount], currency_code), 'currencyID' => currency_code unless options[:amount].blank?
414
+ end
415
+ end
416
+
417
+
418
+ def build_get_balance(return_all_currencies)
419
+ build_request_wrapper('GetBalance') do |xml|
420
+ xml.tag! 'ReturnAllCurrencies', return_all_currencies unless return_all_currencies.nil?
421
+ end
422
+ end
423
+
424
+ def parse(action, xml)
425
+ legacy_hash = legacy_parse(action, xml)
426
+ xml = strip_attributes(xml)
427
+ hash = Hash.from_xml(xml)
428
+ hash = hash.fetch('Envelope').fetch('Body').fetch("#{action}Response")
429
+ hash = hash["#{action}ResponseDetails"] if hash["#{action}ResponseDetails"]
430
+
431
+ legacy_hash.merge(hash)
432
+ rescue IndexError
433
+ legacy_hash.merge(hash['Envelope']['Body'])
434
+ end
435
+
436
+ def strip_attributes(xml)
437
+ xml = REXML::Document.new(xml)
438
+ REXML::XPath.each(xml, '//SOAP-ENV:Envelope//*[@*]') do |el|
439
+ el.attributes.each_attribute { |a| a.remove }
440
+ end
441
+ xml.to_s
442
+ end
443
+
444
+ def legacy_parse(action, xml)
445
+ response = {}
446
+
447
+ error_messages = []
448
+ error_codes = []
449
+
450
+ xml = REXML::Document.new(xml)
451
+ if root = REXML::XPath.first(xml, "//#{action}Response")
452
+ root.elements.each do |node|
453
+ case node.name
454
+ when 'Errors'
455
+ short_message = nil
456
+ long_message = nil
457
+
458
+ node.elements.each do |child|
459
+ case child.name
460
+ when "LongMessage"
461
+ long_message = child.text unless child.text.blank?
462
+ when "ShortMessage"
463
+ short_message = child.text unless child.text.blank?
464
+ when "ErrorCode"
465
+ error_codes << child.text unless child.text.blank?
466
+ end
467
+ end
468
+
469
+ if message = long_message || short_message
470
+ error_messages << message
471
+ end
472
+ else
473
+ legacy_parse_element(response, node)
474
+ end
475
+ end
476
+ response[:message] = error_messages.uniq.join(". ") unless error_messages.empty?
477
+ response[:error_codes] = error_codes.uniq.join(",") unless error_codes.empty?
478
+ elsif root = REXML::XPath.first(xml, "//SOAP-ENV:Fault")
479
+ legacy_parse_element(response, root)
480
+ response[:message] = "#{response[:faultcode]}: #{response[:faultstring]} - #{response[:detail]}"
481
+ end
482
+
483
+ response
484
+ end
485
+
486
+ def legacy_parse_element(response, node)
487
+ if node.has_elements?
488
+ node.elements.each{|e| legacy_parse_element(response, e) }
489
+ else
490
+ response[node.name.underscore.to_sym] = node.text
491
+ node.attributes.each do |k, v|
492
+ response["#{node.name.underscore}_#{k.underscore}".to_sym] = v if k == 'currencyID'
493
+ end
494
+ end
495
+ end
496
+
497
+ def build_request(body)
498
+ xml = Builder::XmlMarkup.new
499
+
500
+ xml.instruct!
501
+ xml.tag! 'env:Envelope', ENVELOPE_NAMESPACES do
502
+ xml.tag! 'env:Header' do
503
+ add_credentials(xml) unless @options[:headers] && @options[:headers]['X-PP-AUTHORIZATION']
504
+ end
505
+
506
+ xml.tag! 'env:Body' do
507
+ xml << body
508
+ end
509
+ end
510
+ xml.target!
511
+ end
512
+
513
+ def add_credentials(xml)
514
+ xml.tag! 'RequesterCredentials', CREDENTIALS_NAMESPACES do
515
+ xml.tag! 'n1:Credentials' do
516
+ xml.tag! 'n1:Username', @options[:login]
517
+ xml.tag! 'n1:Password', @options[:password]
518
+ xml.tag! 'n1:Subject', @options[:subject]
519
+ xml.tag! 'n1:Signature', @options[:signature] unless @options[:signature].blank?
520
+ end
521
+ end
522
+ end
523
+
524
+ def add_address(xml, element, address)
525
+ return if address.nil?
526
+ xml.tag! element do
527
+ xml.tag! 'n2:Name', address[:name]
528
+ xml.tag! 'n2:Street1', address[:address1]
529
+ xml.tag! 'n2:Street2', address[:address2]
530
+ xml.tag! 'n2:CityName', address[:city]
531
+ xml.tag! 'n2:StateOrProvince', address[:state].blank? ? 'N/A' : address[:state]
532
+ xml.tag! 'n2:Country', address[:country]
533
+ xml.tag! 'n2:Phone', address[:phone] unless address[:phone].blank?
534
+ xml.tag! 'n2:PostalCode', address[:zip]
535
+ end
536
+ end
537
+
538
+ def add_payment_details_items_xml(xml, options, currency_code)
539
+ options[:items].each do |item|
540
+ xml.tag! 'n2:PaymentDetailsItem' do
541
+ xml.tag! 'n2:Name', item[:name]
542
+ xml.tag! 'n2:Number', item[:number]
543
+ xml.tag! 'n2:Quantity', item[:quantity]
544
+ if item[:amount]
545
+ xml.tag! 'n2:Amount', item_amount(item[:amount], currency_code), 'currencyID' => currency_code
546
+ end
547
+ xml.tag! 'n2:Description', item[:description]
548
+ xml.tag! 'n2:ItemURL', item[:url]
549
+ xml.tag! 'n2:ItemCategory', item[:category] if item[:category]
550
+ end
551
+ end
552
+ end
553
+
554
+ def add_payment_details(xml, money, currency_code, options = {})
555
+ xml.tag! 'n2:PaymentDetails' do
556
+ xml.tag! 'n2:OrderTotal', localized_amount(money, currency_code), 'currencyID' => currency_code
557
+
558
+ # All of the values must be included together and add up to the order total
559
+ if [:subtotal, :shipping, :handling, :tax].all?{ |o| options.has_key?(o) }
560
+ xml.tag! 'n2:ItemTotal', localized_amount(options[:subtotal], currency_code), 'currencyID' => currency_code
561
+ xml.tag! 'n2:ShippingTotal', localized_amount(options[:shipping], currency_code),'currencyID' => currency_code
562
+ xml.tag! 'n2:HandlingTotal', localized_amount(options[:handling], currency_code),'currencyID' => currency_code
563
+ xml.tag! 'n2:TaxTotal', localized_amount(options[:tax], currency_code), 'currencyID' => currency_code
564
+ end
565
+
566
+ xml.tag! 'n2:InsuranceTotal', localized_amount(options[:insurance_total], currency_code),'currencyID' => currency_code unless options[:insurance_total].blank?
567
+ xml.tag! 'n2:ShippingDiscount', localized_amount(options[:shipping_discount], currency_code),'currencyID' => currency_code unless options[:shipping_discount].blank?
568
+ xml.tag! 'n2:InsuranceOptionOffered', options[:insurance_option_offered] if options.has_key?(:insurance_option_offered)
569
+
570
+ xml.tag! 'n2:OrderDescription', options[:description] unless options[:description].blank?
571
+
572
+ # Custom field Character length and limitations: 256 single-byte alphanumeric characters
573
+ xml.tag! 'n2:Custom', options[:custom] unless options[:custom].blank?
574
+
575
+ xml.tag! 'n2:InvoiceID', (options[:order_id] || options[:invoice_id]) unless (options[:order_id] || options[:invoice_id]).blank?
576
+ xml.tag! 'n2:ButtonSource', application_id.to_s.slice(0,32) unless application_id.blank?
577
+
578
+ # The notify URL applies only to DoExpressCheckoutPayment.
579
+ # This value is ignored when set in SetExpressCheckout or GetExpressCheckoutDetails
580
+ xml.tag! 'n2:NotifyURL', options[:notify_url] unless options[:notify_url].blank?
581
+
582
+ add_address(xml, 'n2:ShipToAddress', options[:shipping_address]) unless options[:shipping_address].blank?
583
+
584
+ add_payment_details_items_xml(xml, options, currency_code) unless options[:items].blank?
585
+
586
+ add_express_only_payment_details(xml, options) if options[:express_request]
587
+
588
+ # Any value other than Y – This is not a recurring transaction
589
+ # To pass Y in this field, you must have established a billing agreement with
590
+ # the buyer specifying the amount, frequency, and duration of the recurring payment.
591
+ # requires version 80.0 of the API
592
+ xml.tag! 'n2:Recurring', options[:recurring] unless options[:recurring].blank?
593
+ end
594
+ end
595
+
596
+ def add_express_only_payment_details(xml, options = {})
597
+ add_optional_fields(xml,
598
+ %w{n2:NoteText n2:SoftDescriptor
599
+ n2:TransactionId n2:AllowedPaymentMethodType
600
+ n2:PaymentRequestID n2:PaymentAction},
601
+ options)
602
+ end
603
+
604
+ def add_optional_fields(xml, optional_fields, options = {})
605
+ optional_fields.each do |optional_text_field|
606
+ if optional_text_field =~ /(\w+:)(\w+)/
607
+ ns = $1
608
+ field = $2
609
+ field_as_symbol = field.underscore.to_sym
610
+ else
611
+ ns = ''
612
+ field = optional_text_field
613
+ field_as_symbol = optional_text_field.underscore.to_sym
614
+ end
615
+ xml.tag! ns + field, options[field_as_symbol] unless options[field_as_symbol].blank?
616
+ end
617
+ xml
618
+ end
619
+
620
+ def endpoint_url
621
+ URLS[test? ? :test : :live][@options[:signature].blank? ? :certificate : :signature]
622
+ end
623
+
624
+ def commit(action, request)
625
+ response = parse(action, ssl_post(endpoint_url, build_request(request), @options[:headers]))
626
+
627
+ build_response(successful?(response), message_from(response), response,
628
+ :test => test?,
629
+ :authorization => authorization_from(response),
630
+ :fraud_review => fraud_review?(response),
631
+ :avs_result => { :code => response[:avs_code] },
632
+ :cvv_result => response[:cvv2_code]
633
+ )
634
+ end
635
+
636
+ def fraud_review?(response)
637
+ response[:error_codes] == FRAUD_REVIEW_CODE
638
+ end
639
+
640
+ def authorization_from(response)
641
+ (
642
+ response[:transaction_id] ||
643
+ response[:authorization_id] ||
644
+ response[:refund_transaction_id] ||
645
+ response[:billing_agreement_id]
646
+ )
647
+ end
648
+
649
+ def successful?(response)
650
+ SUCCESS_CODES.include?(response[:ack])
651
+ end
652
+
653
+ def message_from(response)
654
+ response[:message] || response[:ack]
655
+ end
656
+
657
+ def date_to_iso(date)
658
+ (date.is_a?(Date) ? date.to_time : date).utc.iso8601
659
+ end
660
+
661
+ def item_amount(amount, currency_code)
662
+ if amount.to_i < 0 && Gateway.non_fractional_currency?(currency_code)
663
+ amount(amount).to_f.floor
664
+ else
665
+ localized_amount(amount, currency_code)
666
+ end
667
+ end
668
+ end
669
+ end
670
+ end